-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
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.