A Look Behind the for Loop - Oli De Bastiani
The for loop is one of the fundamental control structures in most modern programming languages.<br>It allows a block of code to run for a predetermined number of iterations, making it ideal when the iteration<br>count is known in advance or when iterating over lists by index. This article takes a closer look at how the<br>for loop works and what happens behind the scenes, focusing on C# and .net.
What Can a for Loop Do?
A for loop consists of three parts: an initialization, a condition, and an increment expression.<br>In C# and many other languages, a for loop, looks like this:
for (int i = 0; i
This code prints the numbers 0 through 4. It begins by declaring and initializing the variable i,<br>then checks the condition i . If the condition is true, the loop body executes, followed by<br>the increment expression i++. This process repeats until the condition evaluates to false.
All three parts (initialization, condition, and increment) are optional. The following example is valid C# code,<br>even though it creates an infinite loop (which can still be exited using break):
for (; ; )<br>Console.WriteLine("Hello");
You can also declare multiple variables (of the same type) and include multiple expressions in the increment<br>section. These expressions do not need to be related to the loop itself:
for (int i = 0, j = 10, sum = 0;
The increment section can even contain asynchronous code, like in the following example that shows the time every second for one minute:
for (DateTime targetDateTime = DateTime.Now.AddMinutes(1);<br>DateTime.Now
Remember that just because something can be done doesn’t mean it should be used in production code.
Just a Fancy while Loop?
A for loop can be lowered into a while loop. Consider the following method:
public static void ForExample()<br>Console.WriteLine("Start");
for (int i = 0; i
This can be rewritten using a while loop with identical behavior:
public static void WhileExample()<br>Console.WriteLine("Start");
{ // separate scope for variables<br>int i = 0; // initialization
while (i
The separate scope ensures that variables declared inside the loop are not accessible outside of it.
A while loop can be lowered even further using goto:
public static void GotoExample()<br>Console.WriteLine("Start");
{ // separate scope for variables<br>int i = 0; // initialization
goto LoopCheck;
LoopStart:<br>Console.WriteLine(i); // body
i++; // increment
LoopCheck:<br>if (i
All these versions ultimately compile to the same CIL (Common Intermediate Language). CIL does not have loop<br>constructs, only branching instructions such as br and blt:
IL_0000: ldstr "Start"<br>IL_0005: call void [System.Console]System.Console::WriteLine(string)<br>IL_000a: ldc.i4.0<br>IL_000b: stloc.0<br>IL_000c: br.s IL_0018<br>// loop start (head: IL_0018)<br>IL_000e: ldloc.0<br>IL_000f: call void [System.Console]System.Console::WriteLine(int32)<br>IL_0014: ldloc.0<br>IL_0015: ldc.i4.1<br>IL_0016: add<br>IL_0017: stloc.0
IL_0018: ldloc.0<br>IL_0019: ldc.i4.5<br>IL_001a: blt.s IL_000e<br>// end loop
IL_001c: ldstr "End"<br>IL_0021: call void [System.Console]System.Console::WriteLine(string)<br>IL_0026: ret
Possible Optimizations
The .net runtime can optimize loops when compiling CIL to machine code. See my previous article to learn how to<br>get the generated machine code.
One key optimization is loop hoisting, which moves invariant expressions outside the loop. This includes:
computations where results do not change per iteration
null checks that only need to be performed once
caching the array length instead of reading the Length property every iteration
Another important optimization is range-check elimination (RCE). If the Jitter (Just-in-Time compiler) can prove<br>that array indexing is safe, it removes bounds checks inside the loop.
Consider this example:
for (int i = 0; i
The Jitter hoists values.Length out of the loop and eliminates bounds checks because i is<br>guaranteed to stay within valid range.
More specialized optimizations are also possible. For example, the following method sets all elements of a byte array to zero:
public static void ClearByteArray(byte[] bytes)<br>for (int i = 0; i
On my ARM64 system, the optimized assembly processes four bytes per iteration by writing a single 32‑bit value, reducing<br>both loop iterations and memory writes.
Other optimizations include:
Loop unrolling: expanding multiple iterations into a single, larger loop body
Loop cloning: generating specialized loop versions for different runtime conditions
Loop vectorization: replacing the loop with SIMD instructions (Single Input Multiple Data)
These optimizations come with trade-offs: unrolling and cloning increase code size, and vectorization only applies to<br>specific patterns. I was not able to produce C# examples that reliably triggered these optimizations.<br>Fortunately, .net’s Profile-Guided Optimization (PGO) can apply them dynamically at runtime when beneficial.
Conclusion
The for loop is a versatile and widely used construct in...