NetFabric.Hyperlinq — Zero Allocation

One solution is to use expression trees to generate code at runtime that will have the same behavior.

The extension method GetEnumeratorMethod(), at line 23, uses reflection to look for the most appropriate implementation of GetEnumerator().

Just like foreach, it first looks for a public non-static implementation, which can return some type that is not an interface.

If not found, it then checks for IEnumerable<T> or IEnumerable implementations, that will always return interfaces.

Finding the method is not enough.

It needs to be called.

We could use Invoke() to call it but this is slow.

The method Create(), at line 9, uses expression trees to generates a Func that calls the method returned by GetEnumeratorMethod().

The new method is compiled, making it fast and strongly-typed.

Reflection and expression tree compilation are very slow and should not be used frequently throughout the lifetime of the app.

To avoid this, the resulting Func is stored in a static field, implicitly generated by the read-only property at line 7.

The method Create() is only called the first time the property is accessed.

Because the field is inside a class that uses generics, the compiler will automatically use a different one for each combination of <TEnumerable, TEnumerator, TSource>.

Getting the value from the field is very efficient.

There is no need to use a dictionary that would need to calculate the hash code for Type and look it up in the collision buckets.

We can now implement a new Count() overload that can take advantage of the dynamically generated code.

Notice at line 8 that the cast is not required.

No boxing or any heap allocations happen in this version.

This solution is not compatible with AOT (ahead-of-time compilation)…BenchmarksBenchmarking source.

Count(_ => true), which iterates all the collection items:NetFabric.

Hyperlinq performs much better than LINQ in almost every case.

It can be up to 4 times faster.

Except for when the enumerator is a reference-type, which the performance is fairly the same.

NetFabric.

Hyperlinq only allocates on the heap when the enumerator is a reference-type.

Benchmarking source.

Where(_ => true).

Select(item => item).

Count(), which is the composition of multiple operations and iterates all the collection items:NetFabric.

Hyperlinq maintains more or less the same performance gains.

NetFabric.

Hyperlinq only allocates on the heap when the enumerator is a reference-type and the same size as the previous benchmark, which means it allocates only one enumerator instance.

LINQ increases allocation size compared to the previous benchmark.

ConclusionIt is possible to implement LINQ operations with zero allocation.

NetFabric.

Hyperlinq implements extension methods for all .

NET collections that have value-type enumerators so that it to happen implicitly.

To add support for other collections, just implement equivalent mapping extensions.

It also supplies multiple overloads for each operation so that the best performance can be achieved in each case.

Reference-type enumerables and enumerators will always be allocated on the heap but are still supported.

.

. More details

Leave a Reply