Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return singleton enumerators from IEnumerable.GetEnumerator for empty collections #82499

Merged
merged 5 commits into from Feb 23, 2023

Conversation

stephentoub
Copy link
Member

Change the IEnumerable<T>.GetEnumerator() implementations on our core collection types to special-case Count==0 in order to return a single enumerator instead of allocating one a new each time. This saves an allocation when enumerating these collections via the interface in exchange for an extra length check as part of GetEnumerator.

This changes List<>, Queue<>, Stack<>, LinkedList<>, PriorityQueue<,>, SortedDictionary<,>, SortedList<,>, SortedSet<>, HashSet<>, Dictionary<,>, and ArraySegment<>.

Fixes #59596

Relates to #81523, as this means that code like:

var list = new List<int>();
IEnumerator<int> e = list.GetEnumerator();
list.Add(42);
e.MoveNext();

which would previously have thrown an exception on the MoveNext call will no longer throw an exception.

Method Toolchain Mean Error StdDev Median Ratio Allocated Alloc Ratio
SumEmpty \main\corerun.exe 14.920 ns 0.3240 ns 0.6396 ns 14.642 ns 1.00 40 B 1.00
SumEmpty \pr\corerun.exe 5.496 ns 0.0202 ns 0.0157 ns 5.491 ns 0.36 - 0.00
SumOne \main\corerun.exe 20.182 ns 0.1604 ns 0.1500 ns 20.160 ns 1.00 40 B 1.00
SumOne \pr\corerun.exe 20.068 ns 0.0642 ns 0.0501 ns 20.066 ns 0.99 40 B 1.00
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;

[MemoryDiagnoser(false)]
public partial class Program
{
    static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

    private IEnumerable<int> _empty = new List<int>();
    private IEnumerable<int> _one = new List<int>() { 42 };

    [Benchmark]
    public int SumEmpty()
    {
        int sum = 0;
        foreach (int i in _empty) sum += i;
        return sum;
    }

    [Benchmark]
    public int SumOne()
    {
        int sum = 0;
        foreach (int i in _one) sum += i;
        return sum;
    }
}

… collections

Change the `IEnumerable<T>.GetEnumerator()` implementations on our core collection types to special-case Count==0 in order to return a single enumerator instead of allocating one a new each time.  This saves an allocation when enumerating these collections via the interface in exchange for an extra length check as part of GetEnumerator.
@stephentoub stephentoub added this to the 8.0.0 milestone Feb 22, 2023
@ghost ghost assigned stephentoub Feb 22, 2023
@ghost
Copy link

ghost commented Feb 22, 2023

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

Issue Details

Change the IEnumerable<T>.GetEnumerator() implementations on our core collection types to special-case Count==0 in order to return a single enumerator instead of allocating one a new each time. This saves an allocation when enumerating these collections via the interface in exchange for an extra length check as part of GetEnumerator.

This changes List<>, Queue<>, Stack<>, LinkedList<>, PriorityQueue<,>, SortedDictionary<,>, SortedList<,>, SortedSet<>, HashSet<>, Dictionary<,>, and ArraySegment<>.

Fixes #59596

Relates to #81523, as this means that code like:

var list = new List<int>();
IEnumerator<int> e = list.GetEnumerator();
list.Add(42);
e.MoveNext();

which would previously have thrown an exception on the MoveNext call will no longer throw an exception.

Method Toolchain Mean Error StdDev Median Ratio Allocated Alloc Ratio
SumEmpty \main\corerun.exe 14.920 ns 0.3240 ns 0.6396 ns 14.642 ns 1.00 40 B 1.00
SumEmpty \pr\corerun.exe 5.496 ns 0.0202 ns 0.0157 ns 5.491 ns 0.36 - 0.00
SumOne \main\corerun.exe 20.182 ns 0.1604 ns 0.1500 ns 20.160 ns 1.00 40 B 1.00
SumOne \pr\corerun.exe 20.068 ns 0.0642 ns 0.0501 ns 20.066 ns 0.99 40 B 1.00
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;

[MemoryDiagnoser(false)]
public partial class Program
{
    static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

    private IEnumerable<int> _empty = new List<int>();
    private IEnumerable<int> _one = new List<int>() { 42 };

    [Benchmark]
    public int SumEmpty()
    {
        int sum = 0;
        foreach (int i in _empty) sum += i;
        return sum;
    }

    [Benchmark]
    public int SumOne()
    {
        int sum = 0;
        foreach (int i in _one) sum += i;
        return sum;
    }
}
Author: stephentoub
Assignees: stephentoub
Labels:

area-System.Collections, tenet-performance

Milestone: 8.0.0

Copy link
Member

@eiriktsarpalis eiriktsarpalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

- Create helper function for empty enumerator
- Add tests for singletons
@stephentoub stephentoub merged commit dc6ad37 into dotnet:main Feb 23, 2023
@stephentoub stephentoub deleted the singletonemptyenumerator branch February 23, 2023 23:01
@dotnet dotnet locked as resolved and limited conversation to collaborators Mar 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Eliminate allocations when enumerating empty collections
2 participants