{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreib52zitrsacqonv4dn3zdhvpxzlc6c5hgcuyvqb6aldjmcm5vxixu",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mppnvfs53hd2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiex5lp5g7pmo4exquwwdezc32z7dss3cl4mbcqcbekpq5ttnmstpi"
},
"mimeType": "image/webp",
"size": 72294
},
"path": "/gamedevtoollab/linq-and-zlinq-in-the-unity-6-era-avoiding-gc-allocations-in-large-scale-projects-36c2",
"publishedAt": "2026-07-03T03:05:24.000Z",
"site": "https://dev.to",
"tags": [
"unity3d",
"csharp",
"performance",
"gamedev",
"https://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html",
"https://docs.unity3d.com/6000.0/Documentation/Manual/csharp-compiler.html",
"https://docs.unity3d.com/6000.0/Documentation/Manual/performance-incremental-garbage-collection.html",
"https://docs.unity3d.com/6000.0/Documentation/Manual/webgl-technical-overview.html",
"https://github.com/Cysharp/ZLinq",
"https://github.com/Cysharp/ZLinq#unity",
"https://github.com/Cysharp/ZLinq#additional-operators",
"https://github.com/Cysharp/ZLinq#drop-in-replacement",
"https://unity.com/blog/engine-platform/il2cpp-internals-generic-sharing-implementation"
],
"textContent": "## Introduction\n\nIn large-scale Unity development, GC Alloc can quietly become a real problem.\n\nAt first, nothing looks wrong. But as the project grows and you add more enemies, UI, master data, events, states, notifications, logs, and other systems, small allocations that happen every frame begin to pile up.\n\nLINQ is especially convenient.\n\n\n\n var aliveEnemies = enemies\n .Where(x => x.IsAlive)\n .OrderBy(x => x.DistanceToPlayer)\n .ToList();\n\n\nIt is readable.\nBut if this kind of code runs every frame, it can become a source of both GC Alloc and CPU overhead.\n\nUnity's official documentation also recommends reducing frequent managed heap allocations as much as possible, ideally getting close to 0 bytes per frame.\n\nhttps://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html\n\nFor general GC Alloc best practices, this article refers to the Unity 2022.3 documentation, because the general guidance still applies. Unity 6-specific GC behavior is covered later using the Unity 6.0 documentation.\n\nThis article assumes Unity 6.0 as the minimum Unity version and explains how to choose between regular LINQ and ZLinq in production code.\n\nUnity 6.0 uses the Roslyn C# compiler, and its C# language version is C# 9.0. However, some C# 9 features, such as init-only setters, are not supported.\n\nhttps://docs.unity3d.com/6000.0/Documentation/Manual/csharp-compiler.html\n\n## The short version\n\nThe point of this article is not to ban LINQ completely.\n\n**Do not use LINQ in hot paths just because it is readable.**\n\n**Do not assume ZLinq solves everything just because you introduced it.**\n\nThose are the two main ideas.\n\nA rough guideline looks like this:\n\nArea | Guideline\n---|---\nEditor extensions, build scripts, debug code | Regular LINQ is usually fine\nStartup, loading, initialization | LINQ can be fine, but measure when data size is large\nUpdate / LateUpdate / FixedUpdate | Avoid LINQ by default\nCode that is not per-frame but still called frequently | Consider ZLinq\nCode that materializes results into List or Array | Prefer reusing a preallocated List over LINQ/ZLinq\nBurst / Job / NativeArray-heavy code | This article does not go deep into this area; prefer for loops and Native Collections\n\n## Unity 6 GC assumptions\n\nIn Unity 6, Incremental GC is enabled by default.\nUnity uses the Boehm-Demers-Weiser garbage collector. With Incremental GC, garbage collection work is split across multiple frames to reduce GC spikes.\n\nhttps://docs.unity3d.com/6000.0/Documentation/Manual/performance-incremental-garbage-collection.html\n\nHowever, Incremental GC does not make garbage collection itself faster.\nIt spreads the workload across multiple frames.\n\nUnity's GC is also non-compacting. Older Unity documentation explains that Unity's GC is non-generational, which means it is not good at efficiently handling small and frequent temporary allocations, such as those caused by boxing.\n\nhttps://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html\n\nYou also need to be careful about platform-specific differences.\nUnity 6.0's Web platform documentation explains that GC on Web platforms does not run incrementally over multiple frames like it does on other platforms. Instead, it runs once at the end of every frame.\n\nhttps://docs.unity3d.com/6000.0/Documentation/Manual/webgl-technical-overview.html\n\nIncremental GC also uses write barriers to track reference changes. In code that changes a very large number of references, the overhead of those write barriers may matter.\nThis article focuses on reducing GC Alloc, but the final decision should always be based on profiling a Player build on the target platform.\n\nIn other words, even in Unity 6, the basic policy does not change:\n\n * Reduce per-frame GC Alloc as much as possible\n * Do not underestimate small allocations\n * Avoid allocations in hot paths before GC spikes become a problem\n * Confirm the result with the Profiler\n\n\n\n## Where LINQ tends to become a problem\n\nLINQ is especially risky in code like this:\n\n\n\n void Update()\n {\n var target = _enemies\n .Where(x => x.IsAlive)\n .OrderBy(x => x.DistanceToPlayer)\n .FirstOrDefault();\n\n if (target != null)\n {\n Attack(target);\n }\n }\n\n\nThis is easy to write, but if it runs every frame, it can become problematic.\n\nThe issue is not only GC Alloc.\n\n * Enumeration cost from `Where` and `Select`\n * Sorting cost from `OrderBy`\n * Allocations from `ToList()` and `ToArray()`\n * Closures created by lambdas\n * Enumeration through `IEnumerable<T>`\n * Accumulation of small allocations\n\n\n\nIn a large project, one instance may not matter. The problem is that similar code can appear in dozens of places.\n\nThe result is often not \"someone wrote terrible code\".\nIt is more like \"everyone used a convenient pattern a little bit, and GC Alloc slowly grew everywhere\".\n\n## A case where a for loop is better\n\nThe previous code is safer as a simple for loop if it runs in a hot path.\n\n\n\n private Enemy FindNearestAliveEnemy(IReadOnlyList<Enemy> enemies)\n {\n Enemy nearest = null;\n float nearestDistance = float.MaxValue;\n\n for (int i = 0; i < enemies.Count; i++)\n {\n Enemy enemy = enemies[i];\n\n if (!enemy.IsAlive)\n {\n continue;\n }\n\n float distance = enemy.DistanceToPlayer;\n if (distance < nearestDistance)\n {\n nearest = enemy;\n nearestDistance = distance;\n }\n }\n\n return nearest;\n }\n\n\nIt is longer than LINQ.\nBut what it does is explicit.\n\n * It does not create intermediate enumerables\n * It does not sort\n * It does not create a List\n * It only searches for the minimum value that matches the condition\n\n\n\nThis kind of small difference matters in code that runs every frame during gameplay.\n\nHowever, this applies when you only need the nearest single item.\nIf you need a fully sorted result, such as a ranking display or a list that must be processed in nearest-first order later, `OrderBy` itself is not wrong.\n\nAlso, if `DistanceToPlayer` calculates `Vector3.Distance` or `magnitude` every time in real code, the distance calculation itself may be expensive before LINQ or ZLinq even enters the picture.\nFor nearest-neighbor checks, you may also want to compare squared distance and cache the player's position before the loop.\nIn this article's examples, `DistanceToPlayer` is treated as a lightweight value to keep the code simple.\n\n## What is ZLinq?\n\nZLinq is a zero-allocation-oriented LINQ library by Cysharp.\n\nIt lets you write code that resembles regular LINQ, while using `AsValueEnumerable()` to convert the source into a value-type-based enumerable. The goal is to reduce allocations caused by method chains.\nThe ZLinq README explains that, unlike regular LINQ, adding more method chains does not easily increase allocations, that it is implemented with struct-based `ValueEnumerable`, and that it generally has better base performance than regular LINQ.\n\nhttps://github.com/Cysharp/ZLinq\n\nThe basic form looks like this:\n\n\n\n using ZLinq;\n\n private void UpdateEnemies(List<Enemy> enemies)\n {\n foreach (Enemy enemy in enemies\n .AsValueEnumerable()\n .Where(static x => x.IsAlive))\n {\n enemy.Tick();\n }\n }\n\n\nCompared with regular LINQ, `AsValueEnumerable()` is added.\n\n\n\n // Regular LINQ\n foreach (Enemy enemy in enemies.Where(static x => x.IsAlive))\n {\n enemy.Tick();\n }\n\n // ZLinq\n foreach (Enemy enemy in enemies.AsValueEnumerable().Where(static x => x.IsAlive))\n {\n enemy.Tick();\n }\n\n\nThe code looks similar, but the enumeration mechanism is different.\n\nHowever, the important point is that adding `AsValueEnumerable()` does not magically make everything zero-allocation.\nIf you call `ToList()`, a List is still created. If a lambda captures an external variable, allocations related to closures can still occur.\n\n## Where ZLinq is easy to use\n\nZLinq works well in cases like this:\n\n\n\n foreach (Enemy enemy in _enemies\n .AsValueEnumerable()\n .Where(static x => x.IsAlive)\n .Where(static x => x.IsVisible))\n {\n enemy.UpdateMarker();\n }\n\n\nThis kind of code is a good fit when you want to:\n\n * Filter\n * Transform\n * Enumerate\n * Use the result immediately\n * Avoid `ToList()`\n\n\n\nIn large projects, API boundaries often use types such as `IEnumerable<T>` or `IReadOnlyList<T>`.\nAbstract types give callers more flexibility, but in hot paths, the fact that the static type is abstract can make profiling and optimization harder.\n\n\n\n private void ApplyDamage(IEnumerable<Enemy> enemies)\n {\n foreach (Enemy enemy in enemies)\n {\n enemy.Damage(10);\n }\n }\n\n\nEnumeration through an abstract type like this can cause extra allocations depending on the implementation and how the code is written.\n\nZLinq explains that even when `AsValueEnumerable()` is used on `IEnumerable<T>`, it may be able to reduce allocations if the actual object is an array or `List<T>`.\n\nhttps://github.com/Cysharp/ZLinq#unity\n\n\n\n private void ApplyDamage(IEnumerable<Enemy> enemies)\n {\n foreach (Enemy enemy in enemies.AsValueEnumerable())\n {\n enemy.Damage(10);\n }\n }\n\n\nHowever, this is not universal.\nThe optimizations explicitly described in the README generally assume that the actual object is an array, `List<T>`, or a similar supported collection.\nWhen going through an abstract type such as `IReadOnlyList<T>`, the behavior can depend on the static type, the actual object, and the ZLinq version.\nIn hot paths, either make the concrete type clear, such as `List<T>`, an array, or a Native Collection, or always confirm GC Alloc in a Player build.\n\n## Things to avoid even with ZLinq\n\nEven when using ZLinq, code like this still needs care:\n\n\n\n var list = _enemies\n .AsValueEnumerable()\n .Where(static x => x.IsAlive)\n .ToList();\n\n\nThe `Where` chain may avoid allocations, but `ToList()` still creates a List.\n\nIn other words, ZLinq is:\n\n> A way to make LINQ-like processing closer to zero-allocation\n\nIt is not:\n\n> A way to erase all collection creation from the result\n\nZLinq's `ValueEnumerable` also cannot be passed everywhere in the same way as a regular `IEnumerable<T>`.\nIf an existing API requires `IEnumerable<T>`, if you want to store the query result in a field, or if you need to use it across `await` or `yield`, you will eventually need to choose something like `ToArray()`, `ToList()`, or `ToArrayPool()`.\nAt that point, allocation and lifetime management become relevant again.\n\nIf you need a List result every frame, it is often safer to allocate the List ahead of time and reuse it.\n\n\n\n private readonly List<Enemy> _aliveEnemies = new();\n\n private void CollectAliveEnemies(IReadOnlyList<Enemy> enemies)\n {\n _aliveEnemies.Clear();\n\n for (int i = 0; i < enemies.Count; i++)\n {\n Enemy enemy = enemies[i];\n\n if (enemy.IsAlive)\n {\n _aliveEnemies.Add(enemy);\n }\n }\n }\n\n\nThis is not fancy, but it is very strong in large-scale Unity development.\n\nUnity's official documentation also introduces the pattern of keeping and reusing a List instead of creating a new one every frame.\n\nhttps://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html\n\nZLinq also provides APIs such as `CopyTo(List<T>)` for copying results into an existing destination.\n`CopyTo(List<T>)` clears the destination List before filling it, so be careful if you want to preserve the existing contents.\n\nhttps://github.com/Cysharp/ZLinq#additional-operators\n\nThat said, until everyone on the team understands the behavior, `Clear()` + `Add()` is often easier to review for hot-path result collection.\n\nReusable Lists are effective for reducing GC Alloc, but `Clear()` usually keeps the internal array capacity.\nIf a List grows extremely large only at peak moments, consider destroying or shrinking it at scene transitions or loading boundaries.\nAlso, if you let another system keep a reference to a reusable List, the next `Clear()` will change its contents.\nWhen passing such Lists to events, async code, or UI binding, define ownership and lifetime clearly.\n\n## Watch out for closures\n\nWith both LINQ and ZLinq, lambdas can create closures when they capture variables from outside the lambda.\n\n\n\n private void UpdateTargets(float range)\n {\n foreach (Enemy enemy in _enemies\n .AsValueEnumerable()\n .Where(x => x.DistanceToPlayer < range))\n {\n enemy.SetTarget();\n }\n }\n\n\nHere, `range` is a variable outside the lambda.\nEven when using ZLinq, this kind of capture may cause the compiler to generate a closure object or delegate.\nIn other words, even if the ZLinq enumeration itself is close to zero-allocation, the lambda can still cause GC Alloc.\n\nUnity's documentation also recommends minimizing closures and anonymous methods in performance-sensitive code, especially in code that runs every frame.\n\nhttps://docs.unity3d.com/2022.3/Documentation/Manual/performance-garbage-collection-best-practices.html\n\nWhen no capture is needed, use a `static` lambda.\n\n\n\n foreach (Enemy enemy in _enemies\n .AsValueEnumerable()\n .Where(static x => x.IsAlive))\n {\n enemy.Tick();\n }\n\n\nAdding `static` prevents the lambda from capturing variables outside itself.\n\n\n\n float range = 10;\n\n // This captures range, so it cannot be static.\n // .Where(static x => x.DistanceToPlayer < range)\n\n\nIn hot paths, either shape the code so that `static` lambdas can be used, or just write a for loop.\n\n## Be careful with Drop-in Generator\n\nZLinq also has a Drop-in Generator that redirects regular LINQ calls toward ZLinq.\n\nIt is convenient, but in large projects, applying it broadly from the beginning is risky.\n\nThe ZLinq README explains that Drop-in Generator uses a Source Generator to generate extension methods for each type, and that these extension methods take priority over regular LINQ.\nIt also explains that, when using Drop-in Generator in Unity, the minimum Unity version is 2022.3.12f1 because of C# Incremental Source Generator support.\n\nhttps://github.com/Cysharp/ZLinq#drop-in-replacement\n\nhttps://github.com/Cysharp/ZLinq#unity\n\nSince this article assumes Unity 6.0, the version requirement is usually not a problem. But if you apply it to the whole project at once, the following issues can happen:\n\n * It becomes harder to understand the difference from regular LINQ\n * It becomes harder to tell where ZLinq is being used\n * It spreads before the whole team understands the behavior\n * API boundaries that take `IEnumerable<T>` become harder to reason about\n * The impact of dependency updates becomes broader\n\n\n\nAt first, I prefer writing `AsValueEnumerable()` explicitly.\nThat way, code review makes it clear that the code is intentionally using ZLinq.\n\nWhen installing ZLinq through tools such as NuGetForUnity, it is safer to pin the versions of ZLinq itself and Drop-in Generator.\nIn team development, if dependency versions differ between developers, the assumptions used during review and benchmarking can also differ.\n\nIf you use Drop-in Generator, asmdef and namespace boundaries should also be part of the team rules.\nMake it clear in which assemblies ZLinq should take priority over regular LINQ, and avoid accidentally applying it to third-party code or shared libraries.\nSome places may also change return types or available APIs compared with `System.Linq`, so keep migration PRs small and check both profiler results and code diffs step by step.\n\n## Installing ZLinq in Unity\n\nWhen using ZLinq in Unity, the README describes a flow where the main ZLinq package is installed through NuGetForUnity or a similar tool, while Unity-specific features are referenced through the `ZLinq.Unity` package via a Git URL.\nThe Unity package adds support for GameObject and Transform hierarchy traversal, among other things.\n\nhttps://github.com/Cysharp/ZLinq#unity\n\nWhen referencing a package through a Git URL, specify a tag or commit hash when possible, and manage both `Packages/manifest.json` and `packages-lock.json` as a team.\nIf only the NuGet-side ZLinq package is pinned while `ZLinq.Unity` or `ZLinq.DropInGenerator` is floating, behavior may differ subtly between developers or CI environments.\n\nFor example, you can traverse a Transform hierarchy.\nThe following code is intended as a startup or verification example. It is not meant to be moved directly into `Update()`.\n\n\n\n using UnityEngine;\n using ZLinq;\n\n public sealed class TransformSearchSample : MonoBehaviour\n {\n [SerializeField] private Transform _root;\n\n private void Start()\n {\n foreach (Transform child in _root\n .Descendants()\n .Where(static x => x.gameObject.activeSelf))\n {\n Debug.Log(child.name);\n }\n }\n }\n\n\nTransform hierarchy traversal is convenient, but if you run it every frame against a deep hierarchy, the traversal cost itself can become a problem.\n\nZLinq does not make repeated hierarchy searches free.\nIf the search target is large, consider caching or updating by differences.\nAlso, `Debug.Log` itself is expensive at runtime, so do not put it in hot paths.\n\n## Treat NativeArray / Job / Burst as a separate topic\n\nZLinq supports Unity types such as `NativeArray` and `NativeSlice`.\nThe README also explains that with Unity Collections 2.1.1 or later, more Native Collections are supported.\n\nhttps://github.com/Cysharp/ZLinq#unity\n\nHowever, in truly low-level hot paths that use Jobs or Burst, data layout, Native Collections, and Burst compilation constraints often matter more than preserving a LINQ-like style.\n\nSo this article focuses on how to handle LINQ and ZLinq in ordinary Unity runtime code, rather than on whether ZLinq can be used with `NativeArray`.\n\n## Do not expect too much from SIMD in Unity\n\nZLinq has SIMD-related features, but the README explains that SIMD is not used in Unity because Unity references .NET Standard 2.1.\n\nhttps://github.com/Cysharp/ZLinq#unity\n\nFor Unity articles, it is less misleading to describe ZLinq as:\n\n> A way to keep a LINQ-like style while reducing GC Alloc and often improving runtime performance compared with regular LINQ\n\nrather than:\n\n> A magic SIMD-based speedup tool\n\nHowever, if you are aiming for maximum performance, that is a different discussion.\nThe ZLinq README also explains that if you want the best possible performance, you should write the code inline.\nFor hot paths, measure ZLinq and for loops with real data.\n\n## Be careful about code size with IL2CPP / AOT\n\nIn large production titles, you need to check not only Mono execution in the Editor, but also IL2CPP builds.\n\nZLinq uses structs and generics heavily.\nThis can be beneficial for reducing GC Alloc and improving runtime performance, but depending on how it is used, it may affect generated IL2CPP code size, build time, and binary size.\n\nUnity's IL2CPP has generic sharing to reduce code size, but value-type generics may be less shareable in some cases.\n\nhttps://unity.com/blog/engine-platform/il2cpp-internals-generic-sharing-implementation\n\nSo when introducing ZLinq, it is safer to first apply it explicitly to the code that actually has a problem, instead of immediately applying Drop-in Generator to the whole project.\nCheck the following:\n\n * GC Alloc in the Editor\n * Profiler results in a Development Build\n * IL2CPP build time\n * Player size\n * Runtime performance on the target platform\n\n\n\nZLinq is useful, but the wider the introduction scope becomes, the more you need to check its impact on the build output.\n\n## Minimum profiling conditions\n\nThis article repeatedly says \"check it with the Profiler\", but it is important not to decide based only on Editor results.\nEspecially in production titles that use IL2CPP, GC Alloc and CPU time in the Mono Editor are not enough for the final decision.\n\nAt minimum, separate the following checks:\n\n * Use the Editor Profiler first to find where GC Alloc occurs\n * Check the target platform Player with a Development Build\n * If using IL2CPP, check CPU time, GC Alloc, Player size, and build time in an IL2CPP build\n * Deep Profile is useful, but it has large overhead, so do not use it blindly for final numeric judgment\n * Compare regular LINQ, ZLinq, and for loops with the same data size and call count\n * When introducing Drop-in Generator, compare profiler results, Player size, and build time before and after the change, ideally per PR\n\n\n\nAlso, even when GC Alloc becomes 0, CPU cost does not disappear.\nSorting with `OrderBy`, Transform hierarchy traversal, distance calculation, virtual calls, cache misses, and other costs remain.\nZLinq is a powerful option for reducing GC Alloc, but in hot paths, CPU time must be checked as well.\n\n## Suggested team rules\n\nIn large-scale development, rules matter more than individual discipline.\n\nFor example, the following rules are easy to review.\n\n### Places where LINQ is acceptable\n\n * Editor extensions\n * Importers\n * Build scripts\n * Debug code\n * Test code\n * One-time conversion at startup\n * Temporary processing during loading\n\n\n\n### Runtime areas that require confirmation\n\n * `Update`\n * `LateUpdate`\n * `FixedUpdate`\n * Per-frame UI updates\n * Updates for many enemies, bullets, effects, and similar objects\n * Scroll list rebuilding\n * Input handling\n * Camera control\n * State machines that run every frame\n\n\n\nIf LINQ or ZLinq is used in these areas, the author should be able to explain during review why it is acceptable.\n\n### Places where ZLinq is worth considering\n\n * Code that is not per-frame but is still called frequently\n * Code where filtering or transformation should remain readable\n * Code that enumerates results immediately without materializing a List\n * Existing codebases full of LINQ that need gradual GC reduction\n * Search code shared between Editor and Runtime\n\n\n\n### Places where for loops should be preferred\n\n * Truly hot code\n * Code called thousands of times per frame\n * Code that needs early `break`\n * Code that only needs the minimum, maximum, or first matching item\n * Code that reuses result Lists\n * Code strongly tied to Job / Burst / Native Collections\n\n\n\n## Code review checklist\n\nIn code review, it is more practical to ask the following questions than to reject every use of LINQ mechanically.\n\n\n\n var result = source.Where(...).Select(...).ToList();\n\n\nWhen you see code like this, check:\n\n 1. Is it called every frame?\n 2. Is the number of items large?\n 3. Is `ToList()` or `ToArray()` really necessary?\n 4. Is a closure being created?\n 5. Is `OrderBy` really necessary?\n 6. Is it sorting only to call `FirstOrDefault`?\n 7. Can the result List be reused?\n 8. Can ZLinq preserve readability while reducing allocations?\n 9. Would a for loop be clearer?\n 10. Is the ownership and lifetime of a reused List clear?\n 11. Do we need to check the impact on IL2CPP builds?\n\n\n\n`OrderBy().FirstOrDefault()` deserves particular attention.\n\n\n\n var target = enemies\n .Where(static x => x.IsAlive)\n .OrderBy(static x => x.DistanceToPlayer)\n .FirstOrDefault();\n\n\nIf you only need the minimum value, you do not need to sort.\n\n\n\n Enemy target = null;\n float minDistance = float.MaxValue;\n\n for (int i = 0; i < enemies.Count; i++)\n {\n Enemy enemy = enemies[i];\n\n if (!enemy.IsAlive)\n {\n continue;\n }\n\n float distance = enemy.DistanceToPlayer;\n if (distance < minDistance)\n {\n target = enemy;\n minDistance = distance;\n }\n }\n\n\nIn this kind of case, changing the algorithm is more effective than replacing LINQ with ZLinq.\n\n## Summary\n\nLINQ and ZLinq are both useful.\nBut in large-scale Unity development, if you do not decide where they are allowed, convenient code can directly turn into runtime performance problems.\n\nThe policy in this article is simple:\n\n * Avoid LINQ in hot paths\n * Consider ZLinq for frequently called code where readability still matters\n * Be suspicious of `ToList()`, `ToArray()`, closures, and unnecessary `OrderBy`\n * Keep the ZLinq introduction scope controlled, and verify with the Profiler and IL2CPP builds\n\n\n\nZLinq is not a free pass to use LINQ carelessly.\n\nIt is best treated as:\n\n> An option for keeping LINQ readability while still thinking seriously about GC",
"title": "LINQ and ZLinq in the Unity 6 Era: Avoiding GC Allocations in Large-Scale Projects"
}