Pipeline Hazards
A ``hazard'' is a circumstance that arises because of pipelining, that
either will make implementing an instruction difficult or will cause
errors in execution (i.e. the pipelining changes the
semantics of instruction execution). There are three basic types of
hazard, with subdivisions possible. We'll mention the hazards first,
and the fixes later, as they are somewhat orthogonal.
- Structural Hazards
- You can't use a single piece of hardware for two things at once.
A Princeton architecture runs into a structural hazard on
ld or st instructions, since there is only
one path to memory and the pipeline is attempting to fetch an
instruction on every cycle.
- Data Hazards
- An instruction may depend on data that is not available yet.
There are three types of data hazards:
- Read After Write (RAW)
- The code calls for a read to occur after a write, but the
pipeline causes the read to occur first. This is the most common (and
is the only one that can occur in the simple example pipeline).
- Write After Read (WAR)
- The code calls for a write to occur after a read, but the
pipeline causes the write to occur first. This can occur in a longer
pipeline in which some instructions read registers in a late stage
while others write registers in an early stage.
- Write After Write (WAW)
- The code calls for two writes to occur; the pipeline reorders
them. This can occur in a pipeline in which register writes can occur
in more than one pipeline stage.
- Control Hazards
- A decision needs to be made as to whether to take a branch,
before the information to make the decision is available.
There are also three ways to fix a hazard. Going from easiest but
least desireable to most desireable but not always possible, we get:
- Document
- Simply document the instruction set as supporting the semantics
as defined by the pipeline (in other words, punt). This is the origin
of delayed branch and delayed load instructions. It results in
non-intuitive instruction semantics, and is tied to a particular
pipeline implementation. If the implementation changes then the
hazards change, and you find yourself making a screwy semantics work.
You can't just document the new semantics like you did the old, since
you have legacy code to support.
- Stall
- Delay the execution of the second instruction until the hazard is
resolved. This preserves reasonable semantics, but slows things
down.
An improvement on simply stalling which can be used to resolve
structural hazards is to use branch prediction: take a guess as to
whether the branch is likely to be taken or not, and continue
executing with whatever guess you've taken. If you guessed wrong you
cancel any instructions that you started to execute, with a penalty no
greater than what you would have had if you'd just stalled.
- Forward
- The data you need may actually exist somewhere in the system,
just not where you want it to be. This can be resolved by adding
extra data paths, which ``forward'' the data to where it is needed.
This is the ideal solution, since it neither messes up the semantics
nor slows down the system. But, if the data is simply not there yet,
then it can't be done.