Loops and Loop Unrolling
Control Flow and Bit Manipulation

2.2 Loops and Loop Unrolling

Loops are fundamental to repeating operations. However, at the machine instruction level, loops represent constant overhead. Every loop iteration requires the CPU to increment an iterator, compare it against a boundary, and branch back to the start. In this lesson, we will explore loop structures and see how compilers optimize away loop overhead entirely.

Let's look at loop types and compilation optimizations.

Standard Loop Constructs in C++

Modern C++ supports three main loop structures: while loops, do while loops, and for loops (including range based for loops).

For systems code, prefer range based for loops when iterating over containers, as they prevent boundary errors and help the compiler optimize memory accesses:

1. While Loop: Condition is checked before entering
int count{0};
while (count < 5) {
    std::cout << count << '\n';
    ++count;
}

// 2. Range based For Loop: Cleanest, safest iteration over vectors/arrays
std::vector<int> numbers{10, 20, 30};
for (int num : numbers) {
    std::cout << num << '\n';
}
Three loop constructs in C++.

Loop Unrolling: Carpet Stretching for CPUs

When you write a loop that runs a small, fixed number of times, the overhead of checking the index and branching back can consume more CPU cycles than the actual work in the loop body.

To optimize this, compilers perform loop unrolling. The compiler copies the loop body multiple times to reduce or completely eliminate the loop checks.

* The Carpet Metaphor: Think of loop unrolling like a compiler getting tired of rolling and unrolling a carpet to inspect it one inch at a time, deciding instead to stretch the entire carpet flat on the floor to look at it all at once. It uses more space (larger binary file size) but allows you to read it in one continuous scan.

Let's look at what happens under the hood when a loop is unrolled:

Your Source Code
void processColors() {
    for (int i{0}; i < 4; ++i) {
        drawColor(i);
    }
}

// What the Compiler compiles it to (Unrolled!)
void processColorsOptimized() {
    drawColor(0); // No loop counters, no comparisons, no jump branches!
    drawColor(1);
    drawColor(2);
    drawColor(3);
}
Loop unrolling optimization output.

By unrolling the loop, the CPU executes four instructions in a straight line with zero branch prediction checks. Modern compilers (like GCC or Clang) will automatically perform loop unrolling when optimization flags (such as -O2 or -O3) are enabled.

Loop Invariants and Code Motion

Another optimization compilers perform inside loops is code motion. If you compute a value inside a loop that does not depend on the loop iterator, the compiler will hoist it out of the loop so it only executes once:

What you wrote (Inefficient)
for (int i{0}; i < 1000; ++i) {
    double factor{pi * 2.0}; // Calculated 1000 times!
    data[i] *= factor;
}

//  What the Compiler generates (Hoisted!)
double factor{pi * 2.0}; // Hoisted out of the loop automatically
for (int i{0}; i < 1000; ++i) {
    data[i] *= factor;
}
Compiler hoisting loop invariants out of execution cycles.

To write optimal code, help the compiler by marking variables outside loops as const or constexpr, making it clear that they do not change during iteration.

Finished reading this lesson?