Uh oh!
There was an error while loading. Please reload this page.
Remove List<T>.Enumerator.MoveNextRare - #118425
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR optimizes the List<T>.Enumerator implementation by removing the MoveNextRare method to improve JIT stack allocation of enumerators. The change addresses a performance issue where the JIT's stack allocation optimization for enumerators fails when List<T> instances become sufficiently large (around 1000 elements) because the MoveNextRare method is considered cold and not inlined, causing the enumerator to be boxed instead of stack-allocated.
Key changes:
- Inlines the
MoveNextRarelogic directly into theMoveNextmethod - Reorders field declarations and simplifies the enumerator state management
- Changes the end-of-enumeration index marker from
_list._size + 1to-1
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
stephentoub
commented
Aug 6, 2025
@EgorBot -arm -amd -intel usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);[MemoryDiagnoser(false)]publicclassBench{[Benchmark][ArgumentsSource(nameof(GetLists))]publicintSumList(List<int>list){intsum=0;foreach(intiteminlist){sum+=item;}returnsum;}[Benchmark][ArgumentsSource(nameof(GetLists))]publicintSumEnumerable(IEnumerable<int>list){intsum=0;foreach(intiteminlist){sum+=item;}returnsum;}publicstaticIEnumerable<List<int>>GetLists()=>fromcountinnewint[]{1,10,1_000}selectEnumerable.Range(0,count).ToList();} |
stephentoub
commented
Aug 6, 2025
Related to #118420, cc: @AndyAyersMS |
8e5ea19 to
da4eb06Compareda4eb06 to
507e605CompareAndyAyersMS
commented
Aug 6, 2025
FYI there is a similar pattern in |
stephentoub
commented
Aug 6, 2025
@EgorBot -arm -amd -intel usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);[MemoryDiagnoser(false)]publicclassBench{[Benchmark][ArgumentsSource(nameof(GetLists))]publicintSumList(List<int>list){intsum=0;foreach(intiteminlist){sum+=item;}returnsum;}[Benchmark][ArgumentsSource(nameof(GetLists))]publicintSumEnumerable(IEnumerable<int>list){intsum=0;foreach(intiteminlist){sum+=item;}returnsum;}publicstaticIEnumerable<List<int>>GetLists()=>fromcountinnewint[]{1,10,1_000}selectEnumerable.Range(0,count).ToList();} |
stephentoub
commented
Aug 6, 2025
@jkotas, any concerns? |
jkotas
commented
Aug 6, 2025
I assume that this will regress perf for common cases without PGO (NAOT and probably Mono too) since Should we go all the way and mark I do not have a strong opinion either way. This feels like a variant of the code size vs. microbenchmark perf trade off we have faced number of times. |
stephentoub
commented
Aug 6, 2025
@EgorBo or @AndyAyersMS can comment more authoritatively, but it seems like it's still inlineable even without PGO: I can mark it with AggressiveInlining, though, if we want to be more sure it happens. |
jkotas
commented
Aug 6, 2025
Cool, LGTM then. |
AndyAyersMS
commented
Aug 6, 2025
In cases where the jit sees the struct enumerator in IL, it applies a fair number of inlining boosts even without PGO: |
Take 2 on #116150
The JIT work to stack allocate enumerators stops working with
List<T>whenList<T>gets sufficiently long, e.g. around 1000 elements. At that point, profiling sees the MoveNextRare method used for the last MoveNext as being cold and doesn't inline it. With it not inlined, the boxed enumerator escapes, and the enumerator is then not stack allocated.(This change only moves the boundary significantly, from ~1000 elements to ~10000 elements. At ~10000, it starts hitting a new limit, due to OSR.)