Hi. I want to share some experience on creating a enumerator for foreach loop so that it would be as performant as if it was a for loop. As we know, System.Range has a nice syntax sugar: That's why I decided to abuse it as a replacement for a for loop: (prints "12345") When you iterate over something with foreach, Roslyn requires that "something" to have a GetEnumerator method, which should return something, which has bool MoveNext() and T Current. We can add it as an extension method: Theoretically, for is more functional than just going over a sequence of natural numbers. But because so often we see that it deserves its own use case of for. We are going to compare against a for loop with i += inc as a step: In terms of performance, it's identical to the one with i++: Method Iterations Mean Error StdDev i++ 10000 3.380 us 0.0534 us 0.0446 us i += inc 10000 3.385 us 0.0575 us 0.0685 us The code for baseline for: (we loaded all fields/properties onto local variables to avoid repeatitive reading from far RAM) Codegen for it: We only care about codegen of iteration of the loop, ignoring the initial overhead and all other operations around. It might make sense to start with trying the already existing API from BCL: Enumerable.Range, whose source code we can find here. Here's what our benchmark looks like: This code is interpreted by roslyn as No need to study the asm codegen, as long as the benchmarks are pretty illustrative: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.448 us 0.0689 us 0.0896 us ForeachWithEnumerableRange 10000 43.946 us 0.8685 us 1.2456 us The second easiest way is to create a custom generator: Nothing completely new, our enumerator is still a big reference type with a dozen of fields. The performance is even worse than that with Enumerable.Range: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.548 us 0.0661 us 0.1320 us ForeachWithEnumerableRange 10000 51.663 us 0.9103 us 1.2460 us ForeachWithYieldReturn 10000 56.580 us 0.3561 us 0.2780 us Now we are going to write our own enumerator. First of all, it will be a struct. To make it work with foreach, we will need a method bool MoveNext() and a property int Current. To make System.Range iterable with foreach, we are making an extension method: The code of the benching method: and is interpreted by Roslyn as: This time it's not a reference type. Also, because our struct does not implement IDisposable, Roslyn does not wrap it with try-finally block. Let's have a look at the benchmark results: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.477 us 0.0690 us 0.0989 us ForeachWithEnumerableRange 10000 50.501 us 0.9984 us 1.2627 us ForeachWithYieldReturn 10000 53.782 us 1.0583 us 0.9900 us ForeachWithRangeEnumerator 10000 18.061 us 0.1731 us 0.1619 us 18 microseconds per 10k operations. That's much better than the previous attempts, but still worse than for. Let's check the codegen: One loop iteration looks like: Despite the enumerator being a struct, and hence, on stack, it's still read from RAM. Let's see what else we can do! Why...? Because what we want is in fact to put the enumerator itself onto registers instead of reading/writing from/to RAM all the time. But probably because of too many local variables, the JIT compiler decided not to put our struct onto registers, so let us help it a bit. From experiment 3 we have Note, that I replaced foreach with the equivalent code, and added Raw as postfix. In Experiment 3 it's shown how foreach works under the hood - the same as I wrote manually. To reduce the number of local variables, we move the loop to a local function: And run the benchmark: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.498 us 0.0658 us 0.1153 us ForeachWithEnumerableRange 10000 43.313 us 0.8207 us 1.0079 us ForeachWithYieldReturn 10000 56.963 us 1.0638 us 1.0448 us ForeachWithRangeEnumerator 10000 17.931 us 0.2047 us 0.1915 us ForeachWithRangeEnumeratorRaw 10000 17.932 us 0.1486 us 0.1390 us ForeachWithRangeEnumeratorRawWithLocal 10000 3.501 us 0.0678 us 0.0807 us ForeachWithRangeEnumeratorRawWithLocal is the last method we wrote - and it's almost 6 times faster, than the one with the loop inlined into the body! What happened? Let's look at the codegen. In our method the local function hasn't been inlined: And it's good news. Let's have a look at our local function: The four marked lines are what we were trying to do. It's identical to the codegen for the for loop! The first three lines simply load the local variables from stack onto registers. That is why our method is so fast. There is in fact a huge difference between what was generated on windows vs linux. What I wrote before is how it works on Windows. To illustrate the difference I created a gist for comparing codegens for ForeachWithRangeEnumeratorRaw and ForeachWithRangeEnumeratorRawWithLocal. Let me go over the key differences: In case of ForeachWithRangeEnumeratorRaw method, Linux's JIT generates: Whereas Windows' JIT yields this: Unlike that one, in case of ForeachWithRangeEnumeratorRawWithLocal Linux gives in the method, and in local function it generates: Meanwhile, Windows's JIT gives call: and the iteration codegen: (all that codegen see in the gist) First, although we got what we wanted, it's definitely easier to write a for loop than caring about whether the compiler put our values onto registers. Second, it's an interesting example how sometimes inlining can literally ruin the performance (in our case, on Windows it took 5-6 times more time on 10k iterations, 3.5 us vs 18 us). All the code I wrote is on a github repo. Benchmarks The enumerator's implementation Tests (to verify that the benched methods are correct) BenchmarkDotNet - tool for accurate performance measurement Open source contributor