Home

Awesome

HonkPerf.NET

Collection of performant (but limited in usage) replacements for BCL's types and methods;

RefLinq

Summary

RefLinq is like Linq, but it must be only used within a method, so behaves more limited than ref structs. Its benefit - it does not make allocations for enumerators and captured variables (given that you use it properly). Its API does not differ much from that of Linq.

So, basically given that you have some sequence, you do .ToRefLinq() on it, and the rest of API is the same. All API which does not make heap allocations is available.

Example:

var arr = new [] { 1, 2, 3, 4 };

...

var localVar = 5;
var seq =
    arr
    .ToRefLinq()                // magic method
    .Select(c => c + 5)
    .Where(c => c % 2 == 0)
    .Select((c, localVar) => c - 6.0 / localVar, localVar) // capture-less capture
    .Append(3)                  // alloc-less append
    .Append(5)
    .Prepend(3)
    .Concat(arr.ToRefLinq().Select(c => c / 1d))  // concatenation
    ;
return seq.Sum() + seq.Max();

In the example above, no heap allocation happens aside from the one to allocate arr in the first line. Though even that may be optimized if you make stack allocation (and use HonkPerf.NET's FixedReadOnlySpan<T>), but that's on your side.

Similar Libraries

Benchmarks

That's yet another linq library. Why use it? Here's a few benchmarks.

We mark bold the one doing the least allocations and italic the fastest one.

Select & Where

MethodMeanErrorStdDevMedianCode SizeGen 0Allocated
RefLinqCombined782.3 ns9.78 ns9.15 ns782.0 ns1,246 B--
ClassicLinqCombined1,337.9 ns26.57 ns72.73 ns1,318.1 ns2,112 B0.0801256 B
NoAlloqCombined1,370.4 ns27.32 ns46.39 ns1,364.9 ns2,114 B0.026788 B
LinqFasterCombined789.3 ns15.74 ns34.54 ns793.1 ns1,971 B0.42531,336 B
HyperlinqCombined701.8 ns13.51 ns16.60 ns696.8 ns1,506 B0.027788 B
ValueLinqCombined795.3 ns15.84 ns37.03 ns782.2 ns1,364 B0.017256 B
LinqAFCombined656.9 ns13.01 ns23.13 ns650.3 ns1,807 B0.027788 B
StructLinqCombined752.9 ns14.93 ns18.88 ns751.4 ns933 B0.0582184 B

Append & Prepend

MethodMeanErrorStdDevGen 0Allocated
RefLinqBench96.51 ns1.968 ns3.931 ns--
LinqBench248.80 ns4.945 ns11.263 ns0.1938608 B
HyperLinqBench293.72 ns6.392 ns18.746 ns0.2165680 B
LinqAFBench253.92 ns5.094 ns13.509 ns--

Select, Where, Zip, Sum

MethodMeanErrorStdDevMedianCode SizeGen 0Allocated
RefLinqSum1.870 us0.0344 us0.0409 us1.863 us2,714 B--
ClassicLinqSum2.724 us0.0562 us0.1640 us2.679 us3,436 B0.1831584 B
HyperLinqSum2.446 us0.0466 us0.0816 us2.431 us2,780 B0.1755560 B
ValueLinqSum1.574 us0.0311 us0.0553 us1.582 us3,545 B0.0401128 B
LinqAFSum2.268 us0.0453 us0.1201 us2.250 us4,228 B0.0458152 B

Select, Where, Append, Prepend, Concat, Sum, Max

MethodMeanErrorStdDevCode SizeGen 0Allocated
RefLinqCombined4.193 us0.0835 us0.2125 us2,679 B--
ClassicLinqCombined8.053 us0.1582 us0.2971 us3,173 B0.32041,032 B
HyperlinqCombined7.555 us0.1397 us0.2410 us2,491 B0.2975944 B
ValueLinqCombined5.142 us0.1021 us0.1967 us3,903 B0.2823888 B
LinqAFCombined5.938 us0.1128 us0.1299 us5,431 B0.0381120 B

Conclusion

There's no "best" linq library. RefLinq is great at avoiding allocations, but sometimes there are faster alternatives (see benchmarks). It provides good API to avoid allocations on captures: you provide the captured function as a last argument, and accept it in the delegate. It also allows to create value delegates identically to how it's done in other libraries. However, it does not offer hundreds of overloads for different use cases to save a few nanoseconds.

Also, the mentioned libraries have different APIs and different coverage of LINQ methods. Some of them don't require the "magic" method, but they may conflict with LINQ then. Some of them lack methods like Append and Prepend, others don't have Zip, etc. RefLinq is not an exception, and it does not have APIs which would require heap allocation.