Background and motivation
Today all enumerators have to follow given preset:
class Enumerator<T> {
bool MoveNext();
T Current { get; }
}
It works and etc, but it requires to store current value between MoveNext() and Current calls. In case when we iterate over collection using foreach, it results in double storage: one instance in enumerator, another one in immutable variable of foreach. Both variables are equal and are immutable during single iteration.
API Proposal
I believe we can remove the need for intermediate store of the Current value in enumerator's body.
// Direct implementation
class InlineEnumerator<T> {
bool MoveNext(out T current);
}
interface IInlineEnumerable<T> : IEnumerable<T> {
IInlineEnumerator<T> GetEnumerator();
}
//Interface implementation, NO MUTUAL INHERITANCE WITH IEnumerator<T>
interface IInlineEnumerator<T> {
bool MoveNext(out T current);
}
In such case current value is directly written to consumer's variable without the need to store the intermediate value.
Here are my benchmarks on net10.0 for iterable struct of 32 bytes:
| Enumerator type |
AggressiveInlinining |
NoInlining |
| struct |
no noticeable diff |
x2-2.5 faster |
| class |
x2 faster |
x2.5-3 faster |
When it comes to iterable struct of size 64K, InlineEnumerator has almost no performance drop, while normal enumerator suffers a heavy(~99%) performance loss.
API Usage
In case of foreach C# compiler should first check if enumerator has public bool MoveNext(out T current) method or implements IInlineEnumerator<T>. In such case foreach should compile using given method.
struct TestCollection {
public TestCollection GetEnumerator() => this;
public bool MoveNext(out object current);
}
void TestMethod(TestCollection arr) {
foreach (var x in arr) {
...
}
}
// Compiles as
void TestMethod(TestCollection arr) {
var enumerator = arr.GetEnumerator();
while (enumerator.MoveNext(out var current)) {
...
}
}
In case of manual iteration(get enumerator then while(MoveNext)), users may check whether inbound IEnumerable<T> is IInlineEnumerable<T> and then use inlined iteration, but they are not obliged to.
Implementation of IInlineEnumerable<T> is optional. It is responsibility of collection's author to get old and new enumeration along.
Alternative Designs
No response
Risks
Existing code should not be affected by these changes unless any enumerator already exposes MoveNext(out ...), however if user already defined it, then it most likely already used via while instead of foreach, I see no other reason for such method to exist on enumerator otherwise.
Thus only remaining risk is desyncing logic among old and inlined enumerator logic.
Background and motivation
Today all enumerators have to follow given preset:
It works and etc, but it requires to store current value between
MoveNext()andCurrentcalls. In case when we iterate over collection usingforeach, it results in double storage: one instance in enumerator, another one in immutable variable offoreach. Both variables are equal and are immutable during single iteration.API Proposal
I believe we can remove the need for intermediate store of the
Currentvalue in enumerator's body.In such case current value is directly written to consumer's variable without the need to store the intermediate value.
Here are my benchmarks on
net10.0for iterable struct of 32 bytes:When it comes to iterable struct of size 64K,
InlineEnumeratorhas almost no performance drop, while normal enumerator suffers a heavy(~99%) performance loss.API Usage
In case of
foreachC# compiler should first check if enumerator has publicbool MoveNext(out T current)method or implementsIInlineEnumerator<T>. In such caseforeachshould compile using given method.In case of manual iteration(get enumerator then
while(MoveNext)), users may check whether inboundIEnumerable<T>isIInlineEnumerable<T>and then use inlined iteration, but they are not obliged to.Implementation of
IInlineEnumerable<T>is optional. It is responsibility of collection's author to get old and new enumeration along.Alternative Designs
No response
Risks
Existing code should not be affected by these changes unless any enumerator already exposes
MoveNext(out ...), however if user already defined it, then it most likely already used viawhileinstead offoreach, I see no other reason for such method to exist on enumerator otherwise.Thus only remaining risk is desyncing logic among old and inlined enumerator logic.