-
Notifications
You must be signed in to change notification settings - Fork 701
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable non-allocating enumeration for IList<T>
The interface `IList<T>` is used in many APIs and interfaces. Enumerating `IList<T>` with `foreach` always requires a heap allocation. Most concrete implementations of `IList<T>` will be `List<T>` which has a non-allocating struct enumerator. This change adds an `NoAllocEnumerate()` extension method that avoids any enumerator allocation when the concrete `IList<T>` type is determined to be `List<T>` at runtime.
- Loading branch information
1 parent
c7cd11a
commit e5195d3
Showing
6 changed files
with
229 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
#nullable enable | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
|
||
namespace NuGet; | ||
|
||
internal static class NoAllocEnumerateExtensions | ||
{ | ||
/// <summary> | ||
/// Avoids allocating an enumerator when enumerating an <see cref="IList{T}"/> which | ||
/// has concrete type <see cref="List{T}"/>. | ||
/// </summary> | ||
/// <remarks> | ||
/// <para> | ||
/// Like many concrete collection types, <see cref="List{T}"/> provides a struct-based | ||
/// enumerator type (<see cref="List{T}.Enumerator"/>) which the compiler can use in | ||
/// <see langword="foreach" /> statements. When using a struct-based enumerator, no heap allocation occurs during | ||
/// enumeration. This is in contrast to the interface-based enumerator <see cref="IEnumerator{T}"/> which will | ||
/// always be allocated on the heap. | ||
/// </para> | ||
/// <para> | ||
/// This method returns a struct-based enumerator that will avoid any heap allocation if <paramref name="collection"/> | ||
/// (which is declared via interface <see cref="IEnumerable{T}"/>) is actually of concrete type | ||
/// <see cref="List{T}"/> at run-time. If so, it delegates to that type's own struct-based | ||
/// enumerator. | ||
/// </para> | ||
/// <para> | ||
/// If <paramref name="list"/> is not of that concrete type, the returned enumerator falls back to the | ||
/// interface-based enumerator, which will heap allocate. Benchmarking shows the overhead in such cases is low enough | ||
/// to be within the measurement error. | ||
/// </para> | ||
/// </remarks> | ||
/// <example> | ||
/// <code> | ||
/// <![CDATA[IList<string> list = ...; | ||
/// | ||
/// foreach (string item in list.NoAllocEnumerate()) | ||
/// { | ||
/// // ... | ||
/// }]]> | ||
/// </code> | ||
/// </example> | ||
public static OptimisticallyNonAllocatingListEnumerable<T> NoAllocEnumerate<T>(this IList<T> list) | ||
where T : notnull | ||
{ | ||
#pragma warning disable CS0618 // Type or member is obsolete | ||
return new(list); | ||
#pragma warning restore CS0618 // Type or member is obsolete | ||
} | ||
|
||
/// <summary> | ||
/// Provides a struct-based enumerator for use with <see cref="IList{T}"/>. | ||
/// Do not use this type directly. Use <see cref="NoAllocEnumerate{T}(IList{T})"/> instead. | ||
/// </summary> | ||
public readonly ref struct OptimisticallyNonAllocatingListEnumerable<T> | ||
where T : notnull | ||
{ | ||
private readonly IList<T> _collection; | ||
|
||
[Obsolete("Do not construct directly. Use internal static class EnumerableExtensions.NoAllocEnumerate instead.")] | ||
internal OptimisticallyNonAllocatingListEnumerable(IList<T> collection) => _collection = collection; | ||
|
||
public Enumerator GetEnumerator() => new(_collection); | ||
|
||
/// <summary> | ||
/// A struct-based enumerator for use with <see cref="IEnumerable{T}"/>. | ||
/// Do not use this type directly. Use <see cref="NoAllocEnumerateExtensions.NoAllocEnumerate{T}(IList{T})"/> instead. | ||
/// </summary> | ||
public struct Enumerator : IDisposable | ||
{ | ||
private readonly int _enumeratorType; | ||
private readonly IEnumerator<T>? _fallbackEnumerator; | ||
private List<T>.Enumerator _concreteEnumerator; | ||
|
||
internal Enumerator(IList<T> list) | ||
{ | ||
_concreteEnumerator = default; | ||
_fallbackEnumerator = null; | ||
|
||
if (list.Count == 0) | ||
{ | ||
// The collection is empty, just return false from MoveNext. | ||
_enumeratorType = 100; | ||
} | ||
else if (list is List<T> concrete) | ||
{ | ||
_enumeratorType = 0; | ||
_concreteEnumerator = concrete.GetEnumerator(); | ||
} | ||
else | ||
{ | ||
_enumeratorType = 99; | ||
_fallbackEnumerator = list.GetEnumerator(); | ||
} | ||
} | ||
|
||
public T Current | ||
{ | ||
get | ||
{ | ||
return _enumeratorType switch | ||
{ | ||
0 => _concreteEnumerator.Current, | ||
99 => _fallbackEnumerator!.Current, | ||
_ => default!, | ||
}; | ||
} | ||
} | ||
|
||
public bool MoveNext() | ||
{ | ||
return _enumeratorType switch | ||
{ | ||
0 => _concreteEnumerator.MoveNext(), | ||
99 => _fallbackEnumerator!.MoveNext(), | ||
_ => false, | ||
}; | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
switch (_enumeratorType) | ||
{ | ||
case 0: | ||
_concreteEnumerator.Dispose(); | ||
break; | ||
case 99: | ||
_fallbackEnumerator!.Dispose(); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
test/NuGet.Core.Tests/NuGet.Common.Test/NoAllocEnumerateExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using Moq; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using Xunit; | ||
|
||
namespace NuGet.Common.Test; | ||
|
||
public class NoAllocEnumerateExtensionsTests | ||
{ | ||
[Fact] | ||
public void GetOptimisticallyNonAllocatingEnumerable_List() | ||
{ | ||
List<int> list = new() { 0, 1, 2, 3 }; | ||
|
||
ValidateEnumeration(list); | ||
} | ||
|
||
[Fact] | ||
public void GetOptimisticallyNonAllocatingEnumerable_ImmutableList() | ||
{ | ||
ImmutableList<int> list = ImmutableList.Create(0, 1, 2, 3); | ||
|
||
ValidateEnumeration(list); | ||
} | ||
|
||
[Fact] | ||
public void GetOptimisticallyNonAllocatingEnumerable_ImmutableArray() | ||
{ | ||
ImmutableArray<int> list = ImmutableArray.Create(0, 1, 2, 3); | ||
|
||
ValidateEnumeration(list); | ||
} | ||
|
||
[Fact] | ||
public void GetOptimisticallyNonAllocatingEnumerable_Fallback() | ||
{ | ||
int[] array = { 0, 1, 2, 3 }; | ||
|
||
var mock = new Mock<IList<int>>(MockBehavior.Strict); | ||
|
||
mock.SetupGet(o => o.Count) | ||
.Returns(array.Length); | ||
mock.Setup(o => o.GetEnumerator()) | ||
.Returns(((IEnumerable<int>)array).GetEnumerator()); | ||
|
||
ValidateEnumeration(mock.Object); | ||
|
||
mock.Verify(); | ||
} | ||
|
||
[Fact] | ||
public void GetOptimisticallyNonAllocatingEnumerable_OptimizedForEmpty() | ||
{ | ||
var mock = new Mock<IList<int>>(MockBehavior.Strict); | ||
|
||
mock.SetupGet(o => o.Count).Returns(0); | ||
|
||
// NOTE because the source is empty, GetEnumerator should not be called at all. | ||
|
||
foreach (int i in mock.Object.NoAllocEnumerate()) | ||
{ | ||
|
||
} | ||
|
||
mock.Verify(); | ||
} | ||
|
||
private static void ValidateEnumeration(IList<int> collection) | ||
{ | ||
var actual = new List<int>(); | ||
|
||
foreach (var item in collection.NoAllocEnumerate()) | ||
{ | ||
actual.Add(item); | ||
} | ||
|
||
Assert.Equal(4, actual.Count); | ||
Assert.Equal(0, actual[0]); | ||
Assert.Equal(1, actual[1]); | ||
Assert.Equal(2, actual[2]); | ||
Assert.Equal(3, actual[3]); | ||
} | ||
} |