Skip to content
Computer Architecture

Instruction-level parallelism: dependences and register renaming

True, anti and output dependences, register renaming with proof, the performance equation with stalls, loop unrolling, the limits of ILP.

The Computer Systems course showed the five-stage pipeline, which overlaps the execution of successive instructions. But a simple pipeline reaches at most one instruction per cycle, and stops every time an instruction waits for the result of another. To go further one must understand which instructions can really be executed together. In this lesson we classify dependences, prove that only true ones limit parallelism while the others are removed by renaming registers, write the performance equation with stalls and look at loop unrolling.

Instruction-level parallelism

Instruction-level parallelism (ILP) is the possibility of executing several instructions of the same sequential program at the same time. The programmer writes a sequence; the processor must guarantee that the result is the same as executing it one instruction at a time, but it is free to change the order and to overlap everything that does not change the result.

Dependences

Between two instructions, I_i preceding I_j in the program, there are three types of register dependence:

  • true or data dependence (RAW, read after write): I_j reads a register written by I_i. The value must pass from one to the other: I_j cannot compute before I_i has produced its result;
  • anti-dependence (WAR, write after read): I_j writes a register that I_i reads. If I_j wrote first, I_i would read the wrong value;
  • output dependence (WAW, write after write): both write the same register. In the end the value of I_j must remain.

To these are added control dependences, on conditional branches, and dependences through memory, harder to see because addresses are known only during execution.

Dependence graph among four instructions
The red arrows are true dependences (read after write), which force waiting for the result. The blue arrow is an anti-dependence and the orange one an output dependence: they arise only from reusing register r1 and disappear when it is renamed.

True dependences and name dependences

Anti-dependences and output dependences carry no data: they exist only because two different values use the same register name. In the example in the figure, I_3 reuses r_1 for a value that has nothing to do with that of I_1.

Theorem. If every instruction writes its result to a new physical register, never used before, and every read refers to the physical register of the last preceding write in the program, WAR and WAW dependences disappear and only RAW ones remain.

Proof. A WAW requires two writes to the same register: with a new physical register for every write there are none. A WAR requires I_j to write a register that I_i reads; but I_i reads the physical register of the write preceding I_i, while I_j writes a new register, different from all existing ones, hence different from the one read. Finally, every read finds the value of the write that precedes it in the program, i.e. the same value as in sequential execution: the result does not change. ∎

This is register renaming. The processor has many more physical registers (hundreds) than the architectural ones visible to the programmer (32 in RISC-V), and a register alias table (RAT) that says, for every architectural register, which physical register holds the most recent value. In the example, after renaming, I_3 and I_1 are independent and can be executed together: only the red arrows remain.

The performance equation with stalls

Execution time is T = N_I\cdot CPI\cdot T_{clk} (lesson on von Neumann and performance). In a pipeline the ideal CPI is 1, and every stall increases it:

    \[ CPI = CPI_{ideal} + \sum_k f_k\,p_k \]

where f_k is the frequency (per instruction) of a certain type of hazard and p_k the number of cycles lost each time. For example, if 25 % of instructions are loads from memory and half of them are immediately followed by an instruction using the data, with one cycle lost each time, the contribution is 0.25\cdot 0.5\cdot 1 = 0.125: the CPI goes from 1 to 1.125, and the processor is 12.5 % slower.

Loop unrolling

The compiler can increase the available ILP. Consider a loop that adds a constant to every element of an array: each iteration loads, adds, stores, increments the index and branches. The three useful instructions depend on one another, and empty cycles remain in between, waiting for results.

By unrolling the loop four times, i.e. writing four iterations in the body with different registers, four independent chains are obtained. The compiler interleaves them: while the first addition waits for its load, the loads of the other three are executed. The empty cycles disappear, and the index increment and the branch, executed once every four elements instead of at every element, weigh a quarter as much. The price is longer code and more registers in use. Compilers do it automatically with optimisation options, and the same idea underlies vectorisation (lesson on SIMD and GPUs).

How much ILP is there really

Studies of ideal processors, with infinite units and perfect prediction, find tens of instructions executable in parallel in common programs. Real processors, limited by prediction, windows and memory, exploit 2 to 5 per cycle. The next lessons show the techniques that approach this: out-of-order execution, branch prediction, superscalar processors.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x