SkillAgentSearch skills...

ZLinq

Zero allocation LINQ with LINQ to Span, LINQ to SIMD, and LINQ to Tree (FileSystem, JSON, GameObject, etc.) for all .NET platforms and Unity, Godot.

Install / Use

/learn @Cysharp/ZLinq
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

ZLinq

CI Benchmark NuGet Ask DeepWiki

Zero allocation LINQ with LINQ to Span, LINQ to SIMD, and LINQ to Tree (FileSystem, JSON, GameObject, etc.) for all .NET platforms(netstandard2.0, 2.1, net8, net9) and Unity, Godot.

Unlike regular LINQ, ZLinq doesn't increase allocations when adding more method chains, and it also has higher basic performance. You can check various benchmark patterns at GitHub Actions/Benchmark. ZLinq shows high performance in almost all patterns, with some benchmarks showing overwhelming differences.

As a bonus, LINQ operators and optimizations equivalent to .NET 10 can be used in .NET Framework 4.8 (netstandard2.0) and Unity (netstandard2.1).

dotnet add package ZLinq
using ZLinq;

var seq = source
    .AsValueEnumerable() // only add this line
    .Where(x => x % 2 == 0)
    .Select(x => x * 3);

foreach (var item in seq) { }
  • 99% compatibility with .NET 10's LINQ (including new Shuffle, RightJoin, LeftJoin, Sequence, InfiniteSequence operators)
  • Zero allocation for method chains through struct-based Enumerable via ValueEnumerable
  • LINQ to Span to full support LINQ operations on Span<T> using .NET 9/C# 13's allows ref struct
  • LINQ to Tree to extend tree-structured objects (built-in support for FileSystem, JSON, GameObject)
  • LINQ to SIMD to automatic application of SIMD where possible and customizable arbitrary operations
  • Optional Drop-in replacement Source Generator to automatically accelerate all LINQ methods

In ZLinq, we have proven high compatibility and performance by running dotnet/runtime's System.Linq.Tests as a drop-in replacement, passing 9000 tests.

Previously, value type-based LINQ implementations were often experimental, but ZLinq fully implements all methods to completely replace standard LINQ in production use, delivering high performance suitable even for demanding applications like games. The performance aspects are based on my experience with previous LINQ implementations (linq.js, SimdLinq, UniRx, R3), zero-allocation implementations (ZString, ZLogger), and high-performance serializers (MessagePack-CSharp, MemoryPack).

ZLinq achieves zero-allocation LINQ implementation using the following structs and interfaces.

public readonly ref struct ValueEnumerable<TEnumerator, T>(TEnumerator enumerator)
    where TEnumerator : struct, IValueEnumerator<T>, allows ref struct
{
    public readonly TEnumerator Enumerator = enumerator;
}

public interface IValueEnumerator<T> : IDisposable
{
    bool TryGetNext(out T current); // as MoveNext + Current

    // Optimization helper
    bool TryGetNonEnumeratedCount(out int count);
    bool TryGetSpan(out ReadOnlySpan<T> span);
    bool TryCopyTo(scoped Span<T> destination, Index offset);
}

Besides changing to a struct-based approach, we've integrated MoveNext and Current to reduce the number of iterator calls. Also, some operators don't need to hold Current, which allows minimizing the struct size. Additionally, being struct-based, we efficiently separate internal state by copying the Enumerator instead of using GetEnumerator. With .NET 9/C# 13 or later, allows ref struct enables natural integration of Span<T> into LINQ.

public static ValueEnumerable<Where<TEnumerator, TSource>, TSource> Where<TEnumerator, TSource>(this ValueEnumerable<TEnumerator, TSource> source, Func<TSource, Boolean> predicate)
    where TEnumerator : struct, IValueEnumerator<TSource>, allows ref struct

Operators have this method signature. C# cannot infer types from generic constraints(dotnet/csharplang#6930). Therefore, the traditional Struct LINQ approach required implementing all operator combinations as instance methods, resulting in 100,000+ methods and massive assembly sizes. However, in ZLinq, we've successfully avoided all the boilerplate method implementations by devising an approach that properly conveys types to C# compiler.

Additionally, TryGetNonEnumeratedCount(out int count), TryGetSpan(out ReadOnlySpan<T> span), and TryCopyTo(Span<T> destination, Index offset) defined in the interface itself enable flexible optimizations. To minimize assembly size, we've designed the library to achieve maximum optimization with minimal method additions. For example, TryCopyTo works efficiently with methods like ToArray when combined with TryGetNonEnumeratedCount. However, it also allows copying to smaller-sized destinations. By combining this with Index, we can optimize First, Last, and ElementAt using just TryCopyTo by passing a single-element Span along with an Index.

If you're interested in architecture, please read my blog post "ZLinq", a Zero-Allocation LINQ Library for .NET where I wrote the details.

Getting Started

You can install package from NuGet/ZLinq. For Unity usage, refer to the Unity section. For Godot usage, refer to the Godot section.

dotnet add package ZLinq

Use using ZLinq; and call AsValueEnumerable() on any iterable type to use ZLinq's zero-allocation LINQ.

using ZLinq;

var source = new int[] { 1, 2, 3, 4, 5 };

// Call AsValueEnumerable to apply ZLinq
var seq1 = source.AsValueEnumerable().Where(x => x % 2 == 0);

// Can also be applied to Span (only in .NET 9/C# 13 environments that support allows ref struct)
Span<int> span = stackalloc int[5] { 1, 2, 3, 4, 5 };
var seq2 = span.AsValueEnumerable().Select(x => x * x);

Even if it's netstandard 2.0 or below .NET 10, all operators up to .NET 10 are available.

You can method chain and foreach like regular LINQ, but there are some limitations. Please see Difference and Limitation for details. ZLinq has drop-in replacements that apply ZLinq without needing to call AsValueEnumerable(). For more information, see Drop-in replacement. Detailed information about LINQ to Tree for LINQ-ifying tree structures (FileSystems and JSON) and LINQ to SIMD for expanding SIMD application range can be found in their respective sections.

Additional Operators

In ZLinq, we prioritize compatibility, so we try to minimize adding custom operators. However, the following methods have been added to enable efficient processing with zero allocation:

AsValueEnumerable()

Converts existing collections to a type that can be chained with ZLinq. Any IEnumerable<T> can be converted, but for the following types, conversion is done with zero allocation without IEnumerable<T>.GetEnumerator() allocation. Standard supported types are T[], List<T>, ArraySegment<T>, Memory<T>, ReadOnlyMemory<T>, ReadOnlySequence<T>, Dictionary<TKey, TValue>, Queue<T>, Stack<T>, LinkedList<T>, HashSet<T>, ImmutableArray<T>, Span<T>, ReadOnlySpan<T>. However, conversion from ImmutableArray<T> requires .NET 8 or higher, and conversion from Span<T>, ReadOnlySpan<T> requires .NET 9 or higher.

When a type is declared as IEnumerable<T> or ICollection<T> rather than concrete types like T[] or List<T>, generally additional allocations occur when using foreach. In ZLinq, even when these interfaces are declared, if the actual type is T[] or List<T>, processing is performed with zero allocation.

Convert from System.Collections.IEnumerable is also supported. In that case, using AsValueEnumerable() without specifying a type converts to ValueEnumerable<, object>, but you can also cast it simultaneously by AsValueEnumerable<T>().

IEnumerable nonGenericCollection = default!;
nonGenericCollection.AsValueEnumerable(); // ValueEnumerable<, object>
nonGenericCollection.AsValueEnumerable<int>(); // ValueEnumerable<, int>

ValueEnumerable.Range(), ValueEnumerable.Repeat(), ValueEnumerable.Empty()

ValueEnumerable.Range operates more efficiently when handling with ZLinq than Enumerable.Range().AsValueEnumerable(). The same applies to Repeat and Empty. The Range can also handle step increments, INumber<T>, DateTime, and more. Please refer to the Range and Sequence section for details.

Sequence, InfiniteSequence for all .NET Platforms

Sequence and InfiniteSequence were added in .NET 10. They require INumber<T>, but INumber<T> was introduced in .NET 7. ZLinq implements INumber<T> methods the same as standard LINQ, but additionaly adds primitive type overloads(byte/sbyte/ushort/char/short/uint/int/ulong/long/float/double/decimal) to support all .NET Platforms(includes .NET Standard 2.0). Additionaly, as a bonus, DateTime and DateTimeOffset overload exists.

Average() : where INumber<T>, Sum() : where INumber<T>

Sys

View on GitHub
GitHub Stars5.0k
CategoryDevelopment
Updated5h ago
Forks206

Languages

C#

Security Score

100/100

Audited on Mar 27, 2026

No findings