Loop unrolling, also known as loop unwinding, is a loop transformation technique that attempts to optimize a program's execution speed at the expense of its binary size, which is an approach known as space–time tradeoff. The transformation can be undertaken manually by the programmer or by an optimizing compiler. On modern processors, loop unrolling is often counterproductive, as the increased code size can cause more cache misses; cf. Duff's device.
The goal of loop unwinding is to increase a program's speed by reducing or eliminating instructions that control the loop, such as pointer arithmetic and "end of loop" tests on each iteration; reducing branch penalties; as well as hiding latencies, including the delay in reading data from memory. To eliminate this computational overhead, loops can be re-written as a repeated sequence of similar independent statements.
Loop unrolling is also part of certain formal verification techniques, in particular bounded model checking.Model Checking Using SMT and Theory of Lists
Advantages The overhead in "tight" loops often consists of instructions to increment a pointer or index to the next element in an array (pointer arithmetic), as well as "end of loop" tests. If an optimizing compiler or assembler is able to pre-calculate offsets to each individually referenced array variable, these can be built into the machine code instructions directly, therefore requiring no additional arithmetic operations at run time.
Significant gains can be realized if the reduction in executed instructions compensates for any performance reduction caused by any increase in the size of the program. Branch penalty is minimized. If the statements in the loop are independent of each other (i.e. where statements that occur earlier in the loop do not affect statements that follow them), the statements can potentially be executed in parallel. Can be implemented dynamically if the number of array elements is unknown at compile time (as in Duff's device).
Optimizing compilers will sometimes perform the unrolling automatically, or upon request.
Disadvantages
Increased Code Size: Unrolling increases the number of instructions, leading to larger program binaries. Higher Storage Requirements: The expanded code takes up more memory, which can be problematic for microcontrollers or embedded systems with limited storage. Instruction Cache Pressure: The unrolled loop consumes more space in the instruction cache. If it exceeds the cache size, frequent cache misses can occur, which can cause severe performance degradation due to costly memory accesses. Reduced Code Readability: If loop unrolling is done manually instead of by an optimizing compiler, the code can become harder to understand and maintain. Conflict with Function Inlining: When the loop body contains function calls, unrolling may prevent inlining due to excessive code expansion, leading to a trade-off between these two optimizations. Increased Register Pressure: On hardware that relies on software pipelining for performance (e.g., systems without register renaming or with in-order superscalar execution), unrolling may require additional registers to store temporary variables across iterations, limiting register reuse. Branch Prediction: Modern CPUs use branch prediction to try to guess which way a branch will go. If the prediction is correct, the CPU can continue executing instructions without waiting for the branch to resolve. However, if the prediction is incorrect, the CPU has to flush the pipeline and start executing the correct instructions, which can be a performance penalty. Loop unrolling can increase the number of branches in the code, which could lead to more branch mispredictions and lower performance. Adam Horvath "Code unwinding - performance is far away"
Static/manual loop unrolling Manual (or static) loop unrolling involves the programmer analyzing the loop and interpreting the iterations into a sequence of instructions which will reduce the loop overhead. This is in contrast to dynamic unrolling which is accomplished by the compiler.
Simple manual example in C A procedure in a computer program is to delete 100 items from a collection. This is normally accomplished by means of a for-loop which calls the function remove(item_number). If this part of the program is to be optimized, and the overhead of the loop requires significant resources compared to those for the remove(x) function, unwinding can be used to speed it up.
Normal loopAfter loop unrolling for (int x = 0; x < 100; x++) { remove(x); } for (int x = 0; x < 100; x += 5) { remove(x); remove(x + 1); remove(x + 2); remove(x + 3); remove(x + 4);
As a result of this modification, the new program has to make only 20 iterations, instead of 100. Afterwards, only 20% of the jumps and conditional branches need to be taken, and represents, over many iterations, a potentially significant decrease in the loop administration overhead. To produce the optimal benefit, no variables should be specified in the unrolled code that require pointer arithmetic. This usually requires "base plus offset" addressing, rather than indexed referencing.
On the other hand, this manual loop unrolling expands the source code size from 3 lines to 7, that have to be produced, checked, and debugged, and the compiler may have to allocate more registers to store variables in the expanded loop iteration . In addition, the loop control variables and number of operations inside the unrolled loop structure have to be chosen carefully so that the result is indeed the same as in the original code (assuming this is a later optimization on already working code). For example, consider the implications if the iteration count were not divisible by 5. The manual amendments required also become somewhat more complicated if the test conditions are variables. See also Duff's device.
Early complexity In the simple case, the loop control is merely an administrative overhead that arranges the productive statements. The loop itself contributes nothing to the results desired, merely saving the programmer the tedium of replicating the code a hundred times which could have been done by a pre-processor generating the replications, or a text editor. Similarly, if-statements and other flow control statements could be replaced by code replication, except that code bloat can be the result. Computer programs easily track the combinations, but programmers find this repetition boring and make mistakes. Consider:
Normal loopAfter loop unrolling for i := 1:8 do if i mod 2 = 0 then do_even_stuff(i) else do_odd_stuff(i); next i; do_odd_stuff(1); do_even_stuff(2); do_odd_stuff(3); do_even_stuff(4); do_odd_stuff(5); do_even_stuff(6); do_odd_stuff(7); do_even_stuff(8);
But of course, the code performed need not be the invocation of a procedure, and this next example involves the index variable in computation:
Normal loopAfter loop unrolling x(1) := 1; for i := 2:9 do x(i) := x(i - 1) * i; print i, x(i); next i; x(1) := 1; x(2) := x(1) * 2; print 2, x(2); x(3) := x(2) * 3; print 3, x(3); x(4) := x(3) * 4; print 4, x(4); ... etc.
which, if compiled, might produce a lot of code (print statements being notorious) but further optimization is possible. This example makes reference only to x(i) and x(i - 1) in the loop (the latter only to develop the new value x(i)) therefore, given that there is no later reference to the array x developed here, its usages could be replaced by a simple variable. Such a change would however mean a simple variable whose value is changed whereas if staying with the array, the compiler's analysis might note that the array's values are constant, each derived from a previous constant, and therefore carries forward the constant values so that the code becomes print 2, 2; print 3, 6; print 4, 24; ...etc.
In general, the content of a loop might be large, involving intricate array indexing. These cases are probably best left to optimizing compilers to unroll. Replicating innermost loops might allow many possible optimisations yet yield only a small gain unless n is large.
Unrolling WHILE loops Consider a pseudocode WHILE loop similar to the following:
Normal loopAfter loop unrollingUnrolled & "tweaked" loop WHILE (condition) DO action ENDWHILE . . . . . . WHILE (condition) DO action IF NOT(condition) THEN GOTO break action IF NOT(condition) THEN GOTO break action ENDWHILE LABEL break: . IF (condition) THEN REPEAT action IF NOT(condition) THEN GOTO break action IF NOT(condition) THEN GOTO break action WHILE (condition) LABEL break: In this case, unrolling is faster because the ENDWHILE (a jump to the start of the loop) will be executed 66% less often.
Even better, the "tweaked" pseudocode example, that may be performed automatically by some optimizing compilers, eliminating unconditional jumps altogether.
Dynamic unrolling Since the benefits of loop unrolling are frequently dependent on the size of an array—which may often not be known until run time—JIT compilers (for example) can determine whether to invoke a "standard" loop sequence or instead generate a (relatively short) sequence of individual instructions for each element. This flexibility is one of the advantages of just-in-time techniques versus static or manual optimization in the context of loop unrolling. In this situation, it is often with relatively small values of n where the savings are still useful—requiring quite small (if any) overall increase in program size (that might be included just once, as part of a standard library).
Assembly language programmers (including optimizing compiler writers) are also able to benefit from the technique of dynamic loop unrolling, using a method similar to that used for efficient branch tables. Here, the advantage is greatest where the maximum offset of any referenced field in a particular array is less than the maximum offset that can be specified in a machine instruction (which will be flagged by the assembler if exceeded).
Assembler example (IBM/360 or Z/Architecture) This example is for IBM/360 or Z/Architecture assemblers and assumes a field of 100 bytes (at offset zero) is to be copied from array FROM to array TO—both having 50 entries with element lengths of 256 bytes each.* The return address is in R14. * Initialize registers R15, R0, R1, and R2 from data defined at the end of * the program starting with label INIT/MAXM1. LM R15,R2,INIT Set R15 = maximum number of MVC * instructions (MAXM1 = 16), * R0 = number of entries of array, * R1 = address of 'FROM' array, and * R2 = address of 'TO' array. * * The loop starts here. LOOP EQU * Define LOOP label. * At this point, R15 will always contain the number 16 (MAXM1). SR R15,R0 Subtract the remaining number of * entries in the array (R0) from R15. BNP ALL If R15 is not positive, meaning we * have more than 16 remaining entries * in the array, jump to do the entire * MVC sequence and then repeat. * * Calculate an offset (from start of MVC sequence) for unconditional branch to * the 'unwound' MVC loop below. * If the number of remaining entries in the arrays is zero, R15 will be 16, so * all the MVC instructions will be bypassed. MH R15,=AL2(ILEN) Multiply R15 by the length of one * MVC instruction. B ALL(R15) Jump to ALL+R15, the address of the * calculated specific MVC instruction * with drop through to the rest of them. * * MVC instruction 'table'. * First entry has maximum allowable offset with single register = hexadecimal F00 * (15*256) in this example. * All 16 of the following MVC ('move character') instructions use base-plus-offset * addressing and each to/from offset decreases by the length of one array element * (256). This avoids pointer arithmetic being required for each element up to a * maximum permissible offset within the instruction of hexadecimal FFF