When you iterate List<T>
and IReadOnlyList<T>
with foreach
statement, IL code will be expanded to following. While List<T>
's List<T>.Enumerator
is struct, IEnumerator<T> e = list.GetEnumerator()
means boxing struct (Enumerator
) to class (IEnumerator<T>
).
This results foeach IReadOnlyList<T>
creates 40B GC allocation.
// List<T>, 0B GC Alloc
List<T>.Enumerator e = list.GetEnumerator();
try { while (e.MoveNext()) { … } }
finally { e.Dispose(); }
// IReadOnlyList<T>, 40B GC Alloc for boxing
IEnumerator<T> e = list.GetEnumerator();
try { while (e.MoveNext()) { … } }
finally { e.Dispose(); }
To avoid allocation, you have 4 choices. 1 should be performant and ZLinq
is the easiest way to accomplish 0 allocation.
- Use
for
statement,IReadOnlyList<T>
can access with indexer.for (var i = 0; i < list.Count; i++) { … }
- Cast to concreate type before iteration.
if (list is List<int> l) foreach (var x in l) { … }
- Prepare method which recieve concreate type of T.
static void Sum<T, TCol>(TCol src) where TCol : IReadOnlyList<T> { foreach (var v in src) total += v; }
- Use struct collection instead of
IReadOnlyList<T>
. ReadOnlyArray for Unityforeach (var x in new UnityEngine.InputSystem.Utilities.ReadOnlyArray<Unit>(array)) { … }
, ZLinqforeach (var x in list.AsValueEnumerable() { … }
Goal | Recommended approach |
---|---|
Iterate at high speed with zero allocations | for (int i = 0; i < list.Count; i++) { … list[i] … } — IReadOnlyList exposes Count and the indexer (this[int] ), so an indexed for loop is allocation-free. |
You can change the parameter type | Accept a concrete type such as List<T> , T[] , or ImmutableArray<T> to avoid boxing. |
Keep the abstract interface but minimize allocations | Use (or implement) a library that returns value-type enumerators, e.g. UnityEngine.InputSystem.Utilities.ReadOnlyArray for Unity. |
If you absolutely must use foreach |
Measure whether the single allocation (≈ 40 B) fits your performance budget; it’s fine if GC pauses remain acceptable. |
// * Summary *
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3775) Unknown processor .NET SDK 9.0.203 [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
IterationCount=3 LaunchCount=1 WarmupCount=3
Alloc Ratio=NA
Method | Runtime | Mean | Error | StdDev | Ratio | Allocated |
---|---|---|---|---|---|---|
ListEnumerator | .NET 8.0 | 5.214 μs | 0.7938 μs | 0.0435 μs | 1.00 | - |
IReadOnlyListEnumerator | .NET 8.0 | 17.779 μs | 2.4332 μs | 0.1334 μs | 3.41 | 40 B |
IReadOnlyListFor | .NET 8.0 | 10.017 μs | 0.6630 μs | 0.0363 μs | 1.92 | - |
IReadOnlyListIs | .NET 8.0 | 6.534 μs | 1.7400 μs | 0.0954 μs | 1.25 | - |
IReadOnlyListZLinq | .NET 8.0 | 23.727 μs | 5.8406 μs | 0.3201 μs | 4.55 | - |
ListEnumerator | .NET 9.0 | 6.479 μs | 0.5099 μs | 0.0280 μs | 1.00 | - |
IReadOnlyListEnumerator | .NET 9.0 | 17.943 μs | 10.6696 μs | 0.5848 μs | 2.77 | 40 B |
IReadOnlyListFor | .NET 9.0 | 10.097 μs | 0.5674 μs | 0.0311 μs | 1.56 | - |
IReadOnlyListIs | .NET 9.0 | 6.393 μs | 1.0105 μs | 0.0554 μs | 0.99 | - |
IReadOnlyListZLinq | .NET 9.0 | 23.227 μs | 3.3263 μs | 0.1823 μs | 3.59 | - |
// * Hints * HideColumnsAnalyser Summary -> Hidden columns: Job, RatioSD, Alloc Ratio
// * Legends * Mean : Arithmetic mean of all measurements Error : Half of 99.9% confidence interval StdDev : Standard deviation of all measurements Ratio : Mean of the ratio distribution ([Current]/[Baseline]) Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) 1 μs : 1 Microsecond (0.000001 sec)
// * Diagnostic Output - MemoryDiagnoser *
Unity 6000.1.2f1.
See code for UnityBenchmark.cs