Prototyping ideas around IterableK trait and a allocation free Iterator
C#
1
131 commits
updated Sep 16, 2026
Iterator<A> prototypePrototyping ideas around IterableK trait and an allocation free Iterator. This is prototype code, not for human consumption, for performance and rapid iteration of ideas
The ref-folds branch on the
language-ext repository has been a staging area for new
performance improvements for foldables, iterables, and collections in general.
It all started as a new sub-type to the Foldable<F> trait that takes an additional parameter FS. One that is
a ref struct state value: Foldable<F, FS>. It also has two additional methods:
static abstract FS StepSetup<A>(K<F, A> ta);
static abstract bool Step<A>(K<F, A> ta, ref FS refState, out A value);
That allows for iteration of a Foldable<F, FS> like so:
var state = F.StepSetup(foldable);
while (F.Step(state, out var value))
{
// Do something with value
}
This turns out to be faster than using enumerators, even struct based enumerators. And it doesn't require allocation of anything on the heap.
The original Foldable<F> trait
has tons of default implementations (like ForAll, Fold, Contains, etc.), which Foldable<F, FS> can override and provide extremely fast versions
using the Foldable<F, FS>.Step approach. And so, all usages of Foldable (that implement this sub-trait) get
an immediate performance boost without any cost to the end user.
This is awesome. Obviously!
One issue is that other (generalist) code is not going to know that, behind the scenes, there is an FS struct value; one that
can be used and leveraged for higher performamce code. Most generalist code will be written for Foldable<F> not
Foldable<F, FS>. So, they can't access the Step method or the constructor of the state to make their own efficient
implementations. And if they do write for Foldable<F, FS> it will exclude the more generalist Foldable<F> types.
They would have to rely solely on the default implementations in Foldable, which isn't bad, but buries some of the benefits.
So, I want...
IterableK<F>So, part 1 of making this even more general is to create IterableK<F> and IterableK<F, FS>. These
decouple from the notion of foldables and create a standalone concept of iterables. They're very similar
concepts and Foldable<F> will derive from IterableK<F>, but they're supposed to just encaspsulate
iteration of a structure.
There's the simplified base-trait (without the FS state baked in):
public interface IterableK<out T>
where T : IterableK<T>
{
static abstract Iterator<A> Forward<A>(K<T, A> ta);
}
This will allow something to become iterable without having to deal with ref types, Unsafe casting, or anything like
that. It will be easy to implement:
For example, if you wanted to make Option<A> an IterableK:
public class Option : IterableK<Option>
{
static Iterator<A> Forward<A>(K<Option, A> ta) =>
ta is Option<A> option
? option.Match(Some: Iterator.singleton, None: default)
: default;
}
But, if you start needing sequences, then you'll want to implement IterableK<F, FS>:
public interface IterableK<out T, TS> : IterableK<T>
where T : IterableK<T, TS>
where TS : struct
{
static abstract TS Setup<A>(K<T, A> ta);
static abstract bool StepMutable<A>(K<T, A> ta, ref TS ts, out A value);
static abstract bool StepImmutable<A>(K<T, A> ta, in TS ts, out Iterator<T, TS, A> value);
// Default implementation
static Iterator<A> IterableK<T>.Forward<A>(K<T, A> ta) =>
IterableK.fromIterable<T, TS, A>(ta);
}
By the way, the name
Forward()is for iterators that go from first-item to last,IterableBackKwithBackward()will be for iterators that go from the last-item to the first. Types can opt in to one or both depending on their traits.
Iterator<A>One thing that has been bothering me for a long time is IEnumerator<T> and the general enumerator
pattern of C#. We have no control over the fact enumerators mutate their members.
It is impossible to create an immutable
IEnumerator<T>derived type and have it work with C#.
Just separating out IterableK from Foldable doesn't fundamentally change anything. The critical thing will
be the capability of the Iterator<A> value that is returned from IterableK.Forward() and IterableBackK.Backward().
It will somehow have to leverage the underlyng IterableK<F, FS>.Step* functions whilst being flexible,
immutable, pure, allocation-free and fast ... a tall order!
Currently Iterator<A> (the one that actually exists in lang-ext today) allows the lifting of IEnumerator<A> and tries
its best to make it appear to be an immutable sequence. However, it does not allow multiple evaluation of the same
reference, where repeated iterations yield the same results. The current implemention is impure and isn't
declarative. This can lead to confusion and bugs because users of language-ext expect all types the have pure and
immutable properties.
The iteration is impure by-default because of
IEnumerator<A>. I want to create anIterator<A>that is pure and anIteratorIO<A>that explicitly might return different values on each evaluation.
Iterator<A>I started by creating a type that looked a little like this:
public record Iterator<A>(A Head, Func<Iterator<A>> Tail);
That is the classic FP 'cons list'. Where the tail is lazy, so the rest of the list only evaluates on-demand, not eagerly. But, of course, then I wanted the head to be lazy too:
public abstract record Iterator<A>;
record IteratorCons<A>(A Head, Func<Iterator<A>> Tail) : Iterator<A>;
record IteratorLazy<A>(Func<Iterator<A>> Tail) : Iterator<A>;
And, singletons and empty:
public abstract record Iterator<A>;
record IteratorEmpty<A> : Iterator<A>;
record IteratorSingle<A>(A Head) : Iterator<A>;
record IteratorCons<A>(A Head, Func<Iterator<A>> Tail) : Iterator<A>;
record IteratorLazy<A>(Func<Iterator<A>> Tail) : Iterator<A>;
All of this ends up being a discriminated union. And that's fine. C# is getting unions soon, but it's not trivial to
make this into an efficient union-type. Firstly, it's a reference type, and so it will end up on the heap. Converting
to a struct is possible, but it limits the extensionality of the type. For example, if I wanted to add
IteratorArr<A>(Arr<A> array) that had a more efficient implemention for the Arr<A> type, it would probably require
that I stay with Iterator<A> being a reference type.
Another issue is that the lazy evaluation of the tail means that every single item in the collection will end up
allocating new entries on the heap: the closure and definitely the newly realised tail Iterator<A>.
That's a hell of an overhead. Especially after finding a way to generalise the iteration of collections
using IterableK<F, FS> faster than C# does it itself! Using this approach to Iterator<A> generalisation
will kill those gains and send the project backwards in terms of performance.
So, that's what this prototype is: it's an attempt to create an efficient Iterator<A> that will iterate
any IterableK<F> or IterableK<F, FS> as well as support the union-cases below:
default(Iterator<A>) (allocation free)(A head, Func<Iterator<A>> tail) - (can cause allocations)Func<Iterator<A>> (can cause allocations)IterableK case - (A head, K<F, A> collection, FS state) - (allocation free)That means for the most common use cases (iterating collection types from language-ext) you will
get the performance gains of the IterableK.Step approach. But, if you still need to build some
lazy iterators, you can - they will be slightly less efficent - but at least they're pure unlike
IEnumerator<A>, which brings robustness and new opportunities.
The eagle eyed will notice that the IterableK case above requires type-arguments (F and FS) that
are not in Iterator<A>.
And so, I have created Iterator<F, FS, A>
in the Iterator.TS folder. This
has the following members:
readonly int tag;
readonly A head;
readonly object? obj1;
readonly VirtualTable<A>? vt;
readonly TS space;
tag specifies the case; head is the head value in non-Lazy cases; obj1 generally captures
reference values that are case dependent; and space carries the state for IterableK cases.
This is all well and good and works fine, but really we want an Iterator<A>, not Iterator<F, FS, A>.
It won't be easy to write generic code over iterators if you need to know the internal state types.
Also, it's a bit ugly.
And so, in the Iterator folder,
I have created Iterator<A> which doesn't have those type-parameters. If you look at its members, you
can see that it maps on top of Iterator<F, FS, A> exactly.
readonly int tag;
readonly A head;
readonly object? obj1;
readonly VirtualTable<A>? vt;
readonly Space128 space;
The primary difference is that space has the type Space128 and not TS. This is a placeholder
struct that takes 128 bytes. We can then use Unsafe.As to convert between Iterator<F, FS, A> and Iterator<A>.
As long as TS is smaller than 128 bytes then Iterator<A> will be able to propagte the TS state without knowing
its type.
Iterator<A> has another member: VirtualTable<A>? vt which works like virtual-tables for C# virtual
methods.
public record VirtualTable<T, TS, A> : VirtualTable<A>
where T : IterableK<T, TS>
where TS : struct
{
public override bool Step(object src, ref Space128 space, out Iterator<A> tail)
{
ref var state = ref Unsafe.As<Space128, TS>(ref space);
if (T.Step(ref state, out A h))
{
var t1 = new Iterator<T, TS, A>(in h, src, in state);
ref var t2 = ref Unsafe.As<Iterator<T, TS, A>, Iterator<A>>(ref t1);
tail = t2;
return true;
}
else
{
tail = default!;
return false;
}
}
}
It has a Step method, which is used when the Iterator<A> is set to the IterableK case. This
Step method casts Space128 to TS, runs the strongly-typed IterabkeK.Step and then casts it
back.
It has some risks (if TS is bigger than Space128), but safeguards can be put in for that.
An instance of the VirtualTable<T, TS, A> is then statically cached as a VirtualTable<A>...
public static class VirtualTableCache<T, TS, A>
where T : IterableK<T, TS>
where TS : struct
{
public static readonly VirtualTable<A> Cache = new VirtualTable<T, TS, A>();
}
Whenever an Iterator<T, TS, A> is constructed in a IterableK case, then it sets vt to the
correct VirtualTableCache<T, TS, A>.Cache value:
internal Iterator(in A head, object source, in TS state)
{
tag = 5;
this.head = head;
obj1 = source;
vt = VirtualTableCache<T, TS, A>.Cache;
space = state;
}
So, what's the problem? This is the output of the benchmarks from this console app...
[Benchmark 1] Elapsed: 46520 µs Each: 0.465 ns Memory: 0 bytes Foreach C# array (100,000,000 items)
[Benchmark 2] Elapsed: 43644 µs Each: 0.436 ns Memory: 0 bytes Foreach current LanguageExt Arr<A> (100,000,000 items)
[Benchmark 3] Elapsed: 24453 µs Each: 0.245 ns Memory: 0 bytes IterableK trait stepping (100,000,000 items)
[Benchmark 4] Elapsed: 24268 µs Each: 0.243 ns Memory: 0 bytes Foreach Array<A> (100,000,000 items)
[Benchmark 5] Elapsed: 488311 µs Each: 4.883 ns Memory: 0 bytes Strong Iterator while TryGetValue (100,000,000 items)
[Benchmark 6] Elapsed: 2343849 µs Each: 23.438 ns Memory: 0 bytes Weak Iterator while TryGetValue (100,000,000 items)
Each test iterates over 1,000,000,000 items, summing a total value. The Elapsed value is the total time
taken for 1,000,000,000 addition ops and the machinery of the iteration (in microseconds). The Each value is
how many nanoseconds it takes to iterate one value and add to the total.
I'm using basic benchmarking here, but it's good enough to test the concepts before breaking out more advanced benchmarking tools. There are warm-up runs before several final runs that are averaged, which is enough when prototyping.
The first one is the baseline, it is a foreach over a C# array:
int[] arr = ...
foreach (var x in arr)
{
total += x;
}
I'm using arrays for the benchmarks because arrays are highly optimised in C#, they have very little boilerplate or fat. So, a generalised immutable-array type that supports
IterableK, going head-to-head with C#'s built-in arrays and mutable enumerators, is the ultimate test.
The second benchmark is the bespoke struct enumerator from the currently released language-ext (v5.0.0-beta-77). It shows
that bespoke enumeration solutions on immutable data-types can get close to the built-in types. It is fast, but it uses
the GetEnumerator machinery of C#, which means the enumerator must be mutable.
This benchmark is here to show the maximum performance we might expect from enumeration of an immutable data-type.
Arr<int> arr = ...
foreach(var x in arr)
{
total += x;
}
Array<A>The rest of the benchmarks will use the prototype code from this application. They are all using Array<A> which has
been created to test these concepts. It is a generalised immutable-array type that implements the IterableK trait-type.
This is the core definition:
public record Array<A>(A[] Items)
: IterableBase<Array, ArrayState, Array<A>, A>;
As you can see the implementation is trivial.
The trait implementation is a little bit more complex, but not that much:
public partial class Array : IterableK<Array, ArrayState>
{
public static ArrayState Setup<A>(K<Array, A> ta) =>
ta is Array<A> arr
? new ArrayState(arr.Items, 0, arr.Items.Length)
: throw new InvalidCastException();
static bool IterableK<Array, ArrayState>.StepMutable<A>(K<Array, A> ta, ref ArrayState ts, out A value)
{
var index = ts.Index;
var count = ts.Count;
if(index >= count)
{
value = default!;
return false;
}
var items = ts.Items;
ref var array = ref Unsafe.As<object, A[]>(ref items);
ts = new ArrayState(items, index + 1, count);
value = array[index];
return true;
}
static bool IterableK<Array, ArrayState>.StepImmutable<A>(K<Array, A> ta, in ArrayState ts, out Iterator<Array, ArrayState, A> next)
{
var index = ts.Index;
var count = ts.Count;
if(index >= count)
{
next = default!;
return false;
}
var items = ts.Items;
ref var array = ref Unsafe.As<object, A[]>(ref items);
var ts1 = new ArrayState(items, index + 1, count);
ref readonly var value = ref array[index];
next = new Iterator<Array, ArrayState, A>(in value, ta, in ts1);
return true;
}
}
Array<A> inherits from IterableBase which confers some instance-method defaults:
public interface IterableBase<T, TS, TA, A> : K<T, A>
where T : IterableK<T, TS>
where TS : struct
where TA : IterableBase<T, TS, TA, A>
{
Iterator<T, TS, A> Forward()
{
var ta = this;
var i1 = T.Forward(ta);
ref var i2 = ref Unsafe.As<Iterator<A>, Iterator<T, TS, A>>(ref i1);
return i2;
}
IterableKEnumerator<T, TS, A> GetEnumerator() =>
new (this);
ReadOnlySpan<A> AsSpan()
{
var ta = this;
var w = ArrayWriter<A>.Init();
var s = T.Setup(ta);
while (T.Step(ref s, out A x))
{
ArrayWriter<A>.Add(ref w, x);
}
return w.View;
}
IEnumerable<A> AsEnumerable() =>
new IteratorEnumerable<T, TS, A>(this);
}
So, we can easily get a strongly-typed immutable Iterator<T, TS, A> from Forward(); a mutable struct-based enumerator
from GetEnumerator(); and bounce the collection to a ReadOnlySpan<A> at speed. None of this is difficult for the
collection-author (other than the efficient implementation of StepMutable and StepImmutable).
Benchmark
3uses theIterableKtrait module-methods. Benchmarks4and5use the generalised methods fromIterableBase. So, from this point on, these are all benchmarks for the prototype code...
The third benchmark is manually using the IterableK trait methods using the Array<A> type in this prototype.
var array = Array.create(..count);
var state = IterableK.setup<Array, ArrayState, int>(array);
while (IterableK.stepMutable<Array, ArrayState, int>(ref state, out var x))
{
total += x;
}
It is about twice as fast as the built-in C# array enumerator, but completely generalised over the IterableK trait!
Generalised code usually loses performance. The numbers do vary run-to-run, but usually this lands somewhere between two times faster and the same speed as C# arrays. That's pretty huge!
As is obvious from the name stepMutable, this uses a mutation of the reference to the state (ref state) to continue
each step of the iteration. And so, this isn't a million miles away from the approach used in IEnumerator. This just
leverages new C# language features, like ref to gain performance.
The fourth benchmark uses GetEnumerator() from Array<A> (which in turn uses a default-implementation from
IterableBase).
var array = Array.create(..count);
foreach(var x in array)
{
total += x;
}
Internally, the returned enumerator, IterableKEnumerator, uses the same StepMutable approach as the previous
benchmark but within a standard Enumerator contract. That allows foreach to be used an other standard enumeration
techniques.
The returned enumerator is a completely generalised struct enumerator, that never needs to be manually written. So,
that means we get the high-performance of the mutable ref approach without any need to build enumerators ourselves.
public struct IterableKEnumerator<T, TS, A>(K<T, A> ta)
where T : IterableK<T, TS>
where TS : struct
{
TS foldState = T.Setup(ta);
A? current;
public bool MoveNext() =>
T.StepMutable(ref foldState, out current);
public void Reset() =>
foldState = T.Setup(ta);
public A Current =>
current!;
}
This uses an Iterator<T, TS, A> which is returned from Forward() (again, leveraging the default-implementation from
IterableBase). It is a completely general iterator, if slightly more awkward to use because of the additional
type-parameters. It it is also immutable and will produce the same results, for the same underlying data structure, every
time. It can be truly treated as a value.
var array = Array.create(..count);
var iter = array.Forward();
while (iter.TryGetValue(out var x, out iter))
{
total += x;
}
This is where it becomes a bit problematic... it is between 6 - 10 times slower. Each iteration takes ~4 ns rather than ~0.4 ns. That's
the difference between 250 million iterations per second and 2.5 billion iterations per second. So, really, it's
still very fast, but it's bugging me that it's slower.
The TryGetValue method is support for the future C# 15 unions.
It is the non-boxing union access pattern and will allow Iterator to be used with fast and efficient pattern-matching.
So, the above could look like this in the future:
while (iter is (var x, iter))
{
total += x;
}
Or, for a really functional style:
public int Sum(Iterator<T, TS, A> iter) =>
iter switch
{
Nil => 0,
(var x, var xs) => x + Sum(xs),
}
If multiple out values are not supported, then we will be able to do this:
public int Sum(Iterator<T, TS, A> iter) =>
iter switch
{
Nil => 0,
Cons(var x, var xs) => x + Sum(xs),
}
Anyway, that's for the future. I want to support that capability as soon as it's availble in C#. But this method is also the slowest out of all of the iteration approaches (well, so far anyway)!
If we look at TryGetValue you can see each possible case being handled by a switch on the tag:
public bool TryGetValue(out A head, out Iterator<T, TS, A> tail)
{
switch (tag)
{
case IteratorTag.IterableK:
head = this.head;
T.StepImmutable(ta!, in space, out tail);
return true;
case IteratorTag.Empty:
head = default!;
tail = default!;
return false;
case IteratorTag.Singleton:
head = this.head;
tail = default;
return true;
case IteratorTag.Cons:
head = this.head;
tail = lazy!();
return true;
case IteratorTag.Lazy:
return lazy!().TryGetValue(out head, out tail);
case IteratorTag.Add:
var first = lazy!();
if (first.TryGetValue(out head, out var nt))
{
tail = new Iterator<T, TS, A>(nt, this.head);
}
else
{
head = this.head;
tail = default;
}
return true;
default:
head = default!;
tail = default!;
return false;
}
}
IteratorTag.IterableK is the IterableK trait case that acquires the tail by calling IterableK.StepImmutable like other benchmarks here.IteratorTag.Empty is an empty iterator.IteratorTag.Singleton is a singleton iterator.IteratorTag.Cons is where the tail is a lazy function that evaluates on-demand (standard Cons case).IteratorTag.Lazy is where the entire iterator is lazy and it needs to be acquired before running TryGetValue on the result.IteratorTag.Add is where the head is an Iterator and the tail is a singleton value (standard Add case).The key area where the performance needs to improve is case IteratorTag.IterableK. It is up to 10 times slower and it's unclear exactly why...
There are things that will be slower:
Iterator<T, TS, A> needs to be constructed.It doesn't feel like these should be a problem, but they seemingy increase the cost of iteration by a factor of 10. It
may well be the cost of immutability and for supporting the TryGetValue union-access pattern.
Benchmark 6 is just like benchmark 5 except that instead of working with Iterator<T, TS, A> it works with Iterator<A>:
var array = Array.create(..count);
var iter = IterableK.fromIterable<Array, ArrayState, int>(array);
while (iter.TryGetValue(out var x, out iter))
{
total += x;
}
This is enabled by IterableK.fromIterable that creates a Iterator<T, TS, A> and then casts the struct to a
Iterator<A>:
public static Iterator<A> fromIterable<T, TS, A>(K<T, A> ta)
where T : IterableK<T, TS>
where TS : struct
{
var s = T.Setup(ta);
return T.StepImmutable(ta, in s, out var i1)
? Unsafe.As<Iterator<T, TS, A>, Iterator<A>>(ref i1)
: default;
}
This uses the VirtualTable to support the casting to Iterator<T, TS, A>. The virtual-calls may be where the overhead lies, that or the Unsafe.As casts.
I've run out of steam a little with this, so if anyone wants to be a hero and make Iterator<T, TS, A> more efficient for
case IteratorTag.IterableK that would be great. And if anyone wants to be a mega-hero to get Iterator<A> into the same
magnitude as Iterator<T, TS, A>, that would be awesome!
Rules:
Unsafe is allowed, don't throw caution to the wind too much!
By the way, I realise that even the slowest benchmark here can iterate over 42 million items per second. Which for many use cases is amazing. And when this technique is paired with other data-types like trees and hash-maps, the iteration part will start to disappear. But still, it feels so damn close to having an allocation free iterator that is guaranteed to be fast.
And I want it! :D
Paul
131 commits
Hacker News (1)
C#
100.0%
Prototyping ideas around IterableK trait and a allocation free Iterator
C#
1
131 commits
updated Sep 16, 2026
Iterator<A> prototypePrototyping ideas around IterableK trait and an allocation free Iterator. This is prototype code, not for human consumption, for performance and rapid iteration of ideas
The ref-folds branch on the
language-ext repository has been a staging area for new
performance improvements for foldables, iterables, and collections in general.
It all started as a new sub-type to the Foldable<F> trait that takes an additional parameter FS. One that is
a ref struct state value: Foldable<F, FS>. It also has two additional methods:
static abstract FS StepSetup<A>(K<F, A> ta);
static abstract bool Step<A>(K<F, A> ta, ref FS refState, out A value);
That allows for iteration of a Foldable<F, FS> like so:
var state = F.StepSetup(foldable);
while (F.Step(state, out var value))
{
// Do something with value
}
This turns out to be faster than using enumerators, even struct based enumerators. And it doesn't require allocation of anything on the heap.
The original Foldable<F> trait
has tons of default implementations (like ForAll, Fold, Contains, etc.), which Foldable<F, FS> can override and provide extremely fast versions
using the Foldable<F, FS>.Step approach. And so, all usages of Foldable (that implement this sub-trait) get
an immediate performance boost without any cost to the end user.
This is awesome. Obviously!
One issue is that other (generalist) code is not going to know that, behind the scenes, there is an FS struct value; one that
can be used and leveraged for higher performamce code. Most generalist code will be written for Foldable<F> not
Foldable<F, FS>. So, they can't access the Step method or the constructor of the state to make their own efficient
implementations. And if they do write for Foldable<F, FS> it will exclude the more generalist Foldable<F> types.
They would have to rely solely on the default implementations in Foldable, which isn't bad, but buries some of the benefits.
So, I want...
IterableK<F>So, part 1 of making this even more general is to create IterableK<F> and IterableK<F, FS>. These
decouple from the notion of foldables and create a standalone concept of iterables. They're very similar
concepts and Foldable<F> will derive from IterableK<F>, but they're supposed to just encaspsulate
iteration of a structure.
There's the simplified base-trait (without the FS state baked in):
public interface IterableK<out T>
where T : IterableK<T>
{
static abstract Iterator<A> Forward<A>(K<T, A> ta);
}
This will allow something to become iterable without having to deal with ref types, Unsafe casting, or anything like
that. It will be easy to implement:
For example, if you wanted to make Option<A> an IterableK:
public class Option : IterableK<Option>
{
static Iterator<A> Forward<A>(K<Option, A> ta) =>
ta is Option<A> option
? option.Match(Some: Iterator.singleton, None: default)
: default;
}
But, if you start needing sequences, then you'll want to implement IterableK<F, FS>:
public interface IterableK<out T, TS> : IterableK<T>
where T : IterableK<T, TS>
where TS : struct
{
static abstract TS Setup<A>(K<T, A> ta);
static abstract bool StepMutable<A>(K<T, A> ta, ref TS ts, out A value);
static abstract bool StepImmutable<A>(K<T, A> ta, in TS ts, out Iterator<T, TS, A> value);
// Default implementation
static Iterator<A> IterableK<T>.Forward<A>(K<T, A> ta) =>
IterableK.fromIterable<T, TS, A>(ta);
}
By the way, the name
Forward()is for iterators that go from first-item to last,IterableBackKwithBackward()will be for iterators that go from the last-item to the first. Types can opt in to one or both depending on their traits.
Iterator<A>One thing that has been bothering me for a long time is IEnumerator<T> and the general enumerator
pattern of C#. We have no control over the fact enumerators mutate their members.
It is impossible to create an immutable
IEnumerator<T>derived type and have it work with C#.
Just separating out IterableK from Foldable doesn't fundamentally change anything. The critical thing will
be the capability of the Iterator<A> value that is returned from IterableK.Forward() and IterableBackK.Backward().
It will somehow have to leverage the underlyng IterableK<F, FS>.Step* functions whilst being flexible,
immutable, pure, allocation-free and fast ... a tall order!
Currently Iterator<A> (the one that actually exists in lang-ext today) allows the lifting of IEnumerator<A> and tries
its best to make it appear to be an immutable sequence. However, it does not allow multiple evaluation of the same
reference, where repeated iterations yield the same results. The current implemention is impure and isn't
declarative. This can lead to confusion and bugs because users of language-ext expect all types the have pure and
immutable properties.
The iteration is impure by-default because of
IEnumerator<A>. I want to create anIterator<A>that is pure and anIteratorIO<A>that explicitly might return different values on each evaluation.
Iterator<A>I started by creating a type that looked a little like this:
public record Iterator<A>(A Head, Func<Iterator<A>> Tail);
That is the classic FP 'cons list'. Where the tail is lazy, so the rest of the list only evaluates on-demand, not eagerly. But, of course, then I wanted the head to be lazy too:
public abstract record Iterator<A>;
record IteratorCons<A>(A Head, Func<Iterator<A>> Tail) : Iterator<A>;
record IteratorLazy<A>(Func<Iterator<A>> Tail) : Iterator<A>;
And, singletons and empty:
public abstract record Iterator<A>;
record IteratorEmpty<A> : Iterator<A>;
record IteratorSingle<A>(A Head) : Iterator<A>;
record IteratorCons<A>(A Head, Func<Iterator<A>> Tail) : Iterator<A>;
record IteratorLazy<A>(Func<Iterator<A>> Tail) : Iterator<A>;
All of this ends up being a discriminated union. And that's fine. C# is getting unions soon, but it's not trivial to
make this into an efficient union-type. Firstly, it's a reference type, and so it will end up on the heap. Converting
to a struct is possible, but it limits the extensionality of the type. For example, if I wanted to add
IteratorArr<A>(Arr<A> array) that had a more efficient implemention for the Arr<A> type, it would probably require
that I stay with Iterator<A> being a reference type.
Another issue is that the lazy evaluation of the tail means that every single item in the collection will end up
allocating new entries on the heap: the closure and definitely the newly realised tail Iterator<A>.
That's a hell of an overhead. Especially after finding a way to generalise the iteration of collections
using IterableK<F, FS> faster than C# does it itself! Using this approach to Iterator<A> generalisation
will kill those gains and send the project backwards in terms of performance.
So, that's what this prototype is: it's an attempt to create an efficient Iterator<A> that will iterate
any IterableK<F> or IterableK<F, FS> as well as support the union-cases below:
default(Iterator<A>) (allocation free)(A head, Func<Iterator<A>> tail) - (can cause allocations)Func<Iterator<A>> (can cause allocations)IterableK case - (A head, K<F, A> collection, FS state) - (allocation free)That means for the most common use cases (iterating collection types from language-ext) you will
get the performance gains of the IterableK.Step approach. But, if you still need to build some
lazy iterators, you can - they will be slightly less efficent - but at least they're pure unlike
IEnumerator<A>, which brings robustness and new opportunities.
The eagle eyed will notice that the IterableK case above requires type-arguments (F and FS) that
are not in Iterator<A>.
And so, I have created Iterator<F, FS, A>
in the Iterator.TS folder. This
has the following members:
readonly int tag;
readonly A head;
readonly object? obj1;
readonly VirtualTable<A>? vt;
readonly TS space;
tag specifies the case; head is the head value in non-Lazy cases; obj1 generally captures
reference values that are case dependent; and space carries the state for IterableK cases.
This is all well and good and works fine, but really we want an Iterator<A>, not Iterator<F, FS, A>.
It won't be easy to write generic code over iterators if you need to know the internal state types.
Also, it's a bit ugly.
And so, in the Iterator folder,
I have created Iterator<A> which doesn't have those type-parameters. If you look at its members, you
can see that it maps on top of Iterator<F, FS, A> exactly.
readonly int tag;
readonly A head;
readonly object? obj1;
readonly VirtualTable<A>? vt;
readonly Space128 space;
The primary difference is that space has the type Space128 and not TS. This is a placeholder
struct that takes 128 bytes. We can then use Unsafe.As to convert between Iterator<F, FS, A> and Iterator<A>.
As long as TS is smaller than 128 bytes then Iterator<A> will be able to propagte the TS state without knowing
its type.
Iterator<A> has another member: VirtualTable<A>? vt which works like virtual-tables for C# virtual
methods.
public record VirtualTable<T, TS, A> : VirtualTable<A>
where T : IterableK<T, TS>
where TS : struct
{
public override bool Step(object src, ref Space128 space, out Iterator<A> tail)
{
ref var state = ref Unsafe.As<Space128, TS>(ref space);
if (T.Step(ref state, out A h))
{
var t1 = new Iterator<T, TS, A>(in h, src, in state);
ref var t2 = ref Unsafe.As<Iterator<T, TS, A>, Iterator<A>>(ref t1);
tail = t2;
return true;
}
else
{
tail = default!;
return false;
}
}
}
It has a Step method, which is used when the Iterator<A> is set to the IterableK case. This
Step method casts Space128 to TS, runs the strongly-typed IterabkeK.Step and then casts it
back.
It has some risks (if TS is bigger than Space128), but safeguards can be put in for that.
An instance of the VirtualTable<T, TS, A> is then statically cached as a VirtualTable<A>...
public static class VirtualTableCache<T, TS, A>
where T : IterableK<T, TS>
where TS : struct
{
public static readonly VirtualTable<A> Cache = new VirtualTable<T, TS, A>();
}
Whenever an Iterator<T, TS, A> is constructed in a IterableK case, then it sets vt to the
correct VirtualTableCache<T, TS, A>.Cache value:
internal Iterator(in A head, object source, in TS state)
{
tag = 5;
this.head = head;
obj1 = source;
vt = VirtualTableCache<T, TS, A>.Cache;
space = state;
}
So, what's the problem? This is the output of the benchmarks from this console app...
[Benchmark 1] Elapsed: 46520 µs Each: 0.465 ns Memory: 0 bytes Foreach C# array (100,000,000 items)
[Benchmark 2] Elapsed: 43644 µs Each: 0.436 ns Memory: 0 bytes Foreach current LanguageExt Arr<A> (100,000,000 items)
[Benchmark 3] Elapsed: 24453 µs Each: 0.245 ns Memory: 0 bytes IterableK trait stepping (100,000,000 items)
[Benchmark 4] Elapsed: 24268 µs Each: 0.243 ns Memory: 0 bytes Foreach Array<A> (100,000,000 items)
[Benchmark 5] Elapsed: 488311 µs Each: 4.883 ns Memory: 0 bytes Strong Iterator while TryGetValue (100,000,000 items)
[Benchmark 6] Elapsed: 2343849 µs Each: 23.438 ns Memory: 0 bytes Weak Iterator while TryGetValue (100,000,000 items)
Each test iterates over 1,000,000,000 items, summing a total value. The Elapsed value is the total time
taken for 1,000,000,000 addition ops and the machinery of the iteration (in microseconds). The Each value is
how many nanoseconds it takes to iterate one value and add to the total.
I'm using basic benchmarking here, but it's good enough to test the concepts before breaking out more advanced benchmarking tools. There are warm-up runs before several final runs that are averaged, which is enough when prototyping.
The first one is the baseline, it is a foreach over a C# array:
int[] arr = ...
foreach (var x in arr)
{
total += x;
}
I'm using arrays for the benchmarks because arrays are highly optimised in C#, they have very little boilerplate or fat. So, a generalised immutable-array type that supports
IterableK, going head-to-head with C#'s built-in arrays and mutable enumerators, is the ultimate test.
The second benchmark is the bespoke struct enumerator from the currently released language-ext (v5.0.0-beta-77). It shows
that bespoke enumeration solutions on immutable data-types can get close to the built-in types. It is fast, but it uses
the GetEnumerator machinery of C#, which means the enumerator must be mutable.
This benchmark is here to show the maximum performance we might expect from enumeration of an immutable data-type.
Arr<int> arr = ...
foreach(var x in arr)
{
total += x;
}
Array<A>The rest of the benchmarks will use the prototype code from this application. They are all using Array<A> which has
been created to test these concepts. It is a generalised immutable-array type that implements the IterableK trait-type.
This is the core definition:
public record Array<A>(A[] Items)
: IterableBase<Array, ArrayState, Array<A>, A>;
As you can see the implementation is trivial.
The trait implementation is a little bit more complex, but not that much:
public partial class Array : IterableK<Array, ArrayState>
{
public static ArrayState Setup<A>(K<Array, A> ta) =>
ta is Array<A> arr
? new ArrayState(arr.Items, 0, arr.Items.Length)
: throw new InvalidCastException();
static bool IterableK<Array, ArrayState>.StepMutable<A>(K<Array, A> ta, ref ArrayState ts, out A value)
{
var index = ts.Index;
var count = ts.Count;
if(index >= count)
{
value = default!;
return false;
}
var items = ts.Items;
ref var array = ref Unsafe.As<object, A[]>(ref items);
ts = new ArrayState(items, index + 1, count);
value = array[index];
return true;
}
static bool IterableK<Array, ArrayState>.StepImmutable<A>(K<Array, A> ta, in ArrayState ts, out Iterator<Array, ArrayState, A> next)
{
var index = ts.Index;
var count = ts.Count;
if(index >= count)
{
next = default!;
return false;
}
var items = ts.Items;
ref var array = ref Unsafe.As<object, A[]>(ref items);
var ts1 = new ArrayState(items, index + 1, count);
ref readonly var value = ref array[index];
next = new Iterator<Array, ArrayState, A>(in value, ta, in ts1);
return true;
}
}
Array<A> inherits from IterableBase which confers some instance-method defaults:
public interface IterableBase<T, TS, TA, A> : K<T, A>
where T : IterableK<T, TS>
where TS : struct
where TA : IterableBase<T, TS, TA, A>
{
Iterator<T, TS, A> Forward()
{
var ta = this;
var i1 = T.Forward(ta);
ref var i2 = ref Unsafe.As<Iterator<A>, Iterator<T, TS, A>>(ref i1);
return i2;
}
IterableKEnumerator<T, TS, A> GetEnumerator() =>
new (this);
ReadOnlySpan<A> AsSpan()
{
var ta = this;
var w = ArrayWriter<A>.Init();
var s = T.Setup(ta);
while (T.Step(ref s, out A x))
{
ArrayWriter<A>.Add(ref w, x);
}
return w.View;
}
IEnumerable<A> AsEnumerable() =>
new IteratorEnumerable<T, TS, A>(this);
}
So, we can easily get a strongly-typed immutable Iterator<T, TS, A> from Forward(); a mutable struct-based enumerator
from GetEnumerator(); and bounce the collection to a ReadOnlySpan<A> at speed. None of this is difficult for the
collection-author (other than the efficient implementation of StepMutable and StepImmutable).
Benchmark
3uses theIterableKtrait module-methods. Benchmarks4and5use the generalised methods fromIterableBase. So, from this point on, these are all benchmarks for the prototype code...
The third benchmark is manually using the IterableK trait methods using the Array<A> type in this prototype.
var array = Array.create(..count);
var state = IterableK.setup<Array, ArrayState, int>(array);
while (IterableK.stepMutable<Array, ArrayState, int>(ref state, out var x))
{
total += x;
}
It is about twice as fast as the built-in C# array enumerator, but completely generalised over the IterableK trait!
Generalised code usually loses performance. The numbers do vary run-to-run, but usually this lands somewhere between two times faster and the same speed as C# arrays. That's pretty huge!
As is obvious from the name stepMutable, this uses a mutation of the reference to the state (ref state) to continue
each step of the iteration. And so, this isn't a million miles away from the approach used in IEnumerator. This just
leverages new C# language features, like ref to gain performance.
The fourth benchmark uses GetEnumerator() from Array<A> (which in turn uses a default-implementation from
IterableBase).
var array = Array.create(..count);
foreach(var x in array)
{
total += x;
}
Internally, the returned enumerator, IterableKEnumerator, uses the same StepMutable approach as the previous
benchmark but within a standard Enumerator contract. That allows foreach to be used an other standard enumeration
techniques.
The returned enumerator is a completely generalised struct enumerator, that never needs to be manually written. So,
that means we get the high-performance of the mutable ref approach without any need to build enumerators ourselves.
public struct IterableKEnumerator<T, TS, A>(K<T, A> ta)
where T : IterableK<T, TS>
where TS : struct
{
TS foldState = T.Setup(ta);
A? current;
public bool MoveNext() =>
T.StepMutable(ref foldState, out current);
public void Reset() =>
foldState = T.Setup(ta);
public A Current =>
current!;
}
This uses an Iterator<T, TS, A> which is returned from Forward() (again, leveraging the default-implementation from
IterableBase). It is a completely general iterator, if slightly more awkward to use because of the additional
type-parameters. It it is also immutable and will produce the same results, for the same underlying data structure, every
time. It can be truly treated as a value.
var array = Array.create(..count);
var iter = array.Forward();
while (iter.TryGetValue(out var x, out iter))
{
total += x;
}
This is where it becomes a bit problematic... it is between 6 - 10 times slower. Each iteration takes ~4 ns rather than ~0.4 ns. That's
the difference between 250 million iterations per second and 2.5 billion iterations per second. So, really, it's
still very fast, but it's bugging me that it's slower.
The TryGetValue method is support for the future C# 15 unions.
It is the non-boxing union access pattern and will allow Iterator to be used with fast and efficient pattern-matching.
So, the above could look like this in the future:
while (iter is (var x, iter))
{
total += x;
}
Or, for a really functional style:
public int Sum(Iterator<T, TS, A> iter) =>
iter switch
{
Nil => 0,
(var x, var xs) => x + Sum(xs),
}
If multiple out values are not supported, then we will be able to do this:
public int Sum(Iterator<T, TS, A> iter) =>
iter switch
{
Nil => 0,
Cons(var x, var xs) => x + Sum(xs),
}
Anyway, that's for the future. I want to support that capability as soon as it's availble in C#. But this method is also the slowest out of all of the iteration approaches (well, so far anyway)!
If we look at TryGetValue you can see each possible case being handled by a switch on the tag:
public bool TryGetValue(out A head, out Iterator<T, TS, A> tail)
{
switch (tag)
{
case IteratorTag.IterableK:
head = this.head;
T.StepImmutable(ta!, in space, out tail);
return true;
case IteratorTag.Empty:
head = default!;
tail = default!;
return false;
case IteratorTag.Singleton:
head = this.head;
tail = default;
return true;
case IteratorTag.Cons:
head = this.head;
tail = lazy!();
return true;
case IteratorTag.Lazy:
return lazy!().TryGetValue(out head, out tail);
case IteratorTag.Add:
var first = lazy!();
if (first.TryGetValue(out head, out var nt))
{
tail = new Iterator<T, TS, A>(nt, this.head);
}
else
{
head = this.head;
tail = default;
}
return true;
default:
head = default!;
tail = default!;
return false;
}
}
IteratorTag.IterableK is the IterableK trait case that acquires the tail by calling IterableK.StepImmutable like other benchmarks here.IteratorTag.Empty is an empty iterator.IteratorTag.Singleton is a singleton iterator.IteratorTag.Cons is where the tail is a lazy function that evaluates on-demand (standard Cons case).IteratorTag.Lazy is where the entire iterator is lazy and it needs to be acquired before running TryGetValue on the result.IteratorTag.Add is where the head is an Iterator and the tail is a singleton value (standard Add case).The key area where the performance needs to improve is case IteratorTag.IterableK. It is up to 10 times slower and it's unclear exactly why...
There are things that will be slower:
Iterator<T, TS, A> needs to be constructed.It doesn't feel like these should be a problem, but they seemingy increase the cost of iteration by a factor of 10. It
may well be the cost of immutability and for supporting the TryGetValue union-access pattern.
Benchmark 6 is just like benchmark 5 except that instead of working with Iterator<T, TS, A> it works with Iterator<A>:
var array = Array.create(..count);
var iter = IterableK.fromIterable<Array, ArrayState, int>(array);
while (iter.TryGetValue(out var x, out iter))
{
total += x;
}
This is enabled by IterableK.fromIterable that creates a Iterator<T, TS, A> and then casts the struct to a
Iterator<A>:
public static Iterator<A> fromIterable<T, TS, A>(K<T, A> ta)
where T : IterableK<T, TS>
where TS : struct
{
var s = T.Setup(ta);
return T.StepImmutable(ta, in s, out var i1)
? Unsafe.As<Iterator<T, TS, A>, Iterator<A>>(ref i1)
: default;
}
This uses the VirtualTable to support the casting to Iterator<T, TS, A>. The virtual-calls may be where the overhead lies, that or the Unsafe.As casts.
I've run out of steam a little with this, so if anyone wants to be a hero and make Iterator<T, TS, A> more efficient for
case IteratorTag.IterableK that would be great. And if anyone wants to be a mega-hero to get Iterator<A> into the same
magnitude as Iterator<T, TS, A>, that would be awesome!
Rules:
Unsafe is allowed, don't throw caution to the wind too much!
By the way, I realise that even the slowest benchmark here can iterate over 42 million items per second. Which for many use cases is amazing. And when this technique is paired with other data-types like trees and hash-maps, the iteration part will start to disappear. But still, it feels so damn close to having an allocation free iterator that is guaranteed to be fast.
And I want it! :D
Paul
Hacker News (1)
131 commits
C#
100.0%