Skip to content

[API Proposal]: Inlined enumerators #124623

@ShadedBlink

Description

@ShadedBlink

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.CollectionsuntriagedNew issue has not been triaged by the area owner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions