Basic Blocks and DAG-Based Optimization for Intermediate Code
In compiler design, a basic block is the main unit used to build a control-flow graph (CFG) and to perform local optimizations. A basic block has:
- single entry: control can enter only at the first statement,
- single exit: execution leaves only at the last statement (e.g., via a jump/branch/return),
- no internal jumps/branch targets except through the entry.
In three-address-code (TAC), basic blocks are identified by choosing leaders: the first instruction, the target of any jump/branch, and the instruction immediately following any jump/branch. The statements from one leader up to (but not including) the next leader form a basic block. This is the standard basis for CFG construction and many compiler optimizations.
For optimizing within a basic block, a DAG is commonly constructed to capture equivalence of computations and dependencies while ensuring no cycles. The DAG nodes typically represent:
- operators for computed expressions,
- variables (leaves/labels),
- optional copy edges for assignments.
This enables:
- common subexpression elimination,
- constant folding,
- and dead code elimination when liveness information is known.
Important: The DAG-based optimization and dead-code elimination are local to the block; they do not require CFG-wide global analysis.
Basic Blocks and DAG-Based Optimization (Intermediate Code)
1) What “basic block” means in the graph representation of intermediate codes
A control-flow graph (CFG) represents possible execution order. Each node is a basic block, and an edge exists if control can flow from one block to another.
Within a basic block, we assume:
- every statement is executed in order (no branches in the middle),
- therefore, local dataflow (like expression reuse) can be performed efficiently.
2) Constructing a DAG for a given basic block
To build a DAG from TAC, process statements sequentially and:
- For each statement of the form
x = y op z:- create an operator node
(op, y, z)if it doesn’t exist, - otherwise reuse the existing operator node,
- then associate/label variable
xwith that operator node (the “value” computed).
- create an operator node
- For assignments
x = y:- create a copy relation (alias) from
xto the same DAG node asy.
- create a copy relation (alias) from
3-address-code variants are well-suited because each statement computes exactly one result.
Mermaid intuition
Construct DAG for the basic block from the given three-address code
- 1Step 1
Given TAC: (1) , (2) , (3) , (4) . Expressions are , , and ; note the redefinition of affects which refers to.
- 2Step 2
Create leaves (variable nodes) for . Each operator node will reference the current DAG value nodes corresponding to the operands.
- 3Step 3
Create operator node for . Label to point to . The available value of used here is the original (before statement 3).
- 4Step 4
Create operator node for . Label to point to . Here is still the original (since statement 3 hasn’t happened).
- 5Step 5
This recomputes but with updated on the left. Since the right side uses the current value (which is still the original at this point), the expression is equivalent to the earlier one in this block; therefore reuse and relabel the variable to point to again.
- 6Step 6
Create operator node for . Label to point to . Also note and refer to their labeled nodes from earlier: and .
- 7Step 7
Operator nodes: for , for , for . Variable labels at the end: ; (though is overwritten later only here); remains a leaf input; and remain labels for their computed nodes as long as used.
DAG structure (explicit)
Below is a clean DAG depiction with the final labeling after processing all statements.
Key points:
- The operator is reused: both statement (1) and statement (3) compute the same expression with the same operand values at that time, so the DAG collapses them.
- The final result is ; therefore the computation depends on and .
Optimize the three-address code assuming only $a$ is live on exit from the block
- 1Step 1
Given: only is live on exit. Therefore, any computation whose result is not needed to compute the final live can be removed.
- 2Step 2
Statement (4) computes from and . So we must know how and are obtained: comes from (2), and comes from (1).
- 3Step 3
Statement (3) reassigns , but statement (4) uses (not ). Since was computed at (1) and is not overwritten, statement (3) does not affect the value of .
- 4Step 4
Drop statement (3) because its result ( after stmt3) is never used to compute the final live variable .
- 5Step 5
Even after dropping (3), we still keep (1) and (2) as they are necessary operands for (4). No further algebraic simplification is possible without additional information.
- 6Step 6
The optimized code is:
How DAG reuses expressions correctly in a block
When an expression like appears multiple times, reuse the same DAG operator node only if the operands represent the same values at that program point (i.e., no intervening redefinition changes their meaning). In this TAC, is not changed until the left side of statement (3), so the right side of (3) uses the same as (1).
Dead-code elimination depends on liveness, not on “harmless looking” redefinitions
Statement (3) looks redundant, but you must confirm whether its computed value is used. Here, is redefined at (3), yet the final live variable depends only on and , so (3) is dead. In other blocks, redefinitions can feed later computations and would not be removable.
Local optimization pipeline within a single basic block
Form basic block
1Partition TAC into leaders/basic blocks (single entry/exit)."
Build expression DAG
2Create operator nodes for expressions and label variables to those nodes."
Mark needed values via liveness
3With only live on exit, mark DAG nodes needed to compute ."
Regenerate optimized TAC
4Emit only the statements producing marked nodes, using DAG reuse to avoid duplicates."
Knowledge Check
A basic block in intermediate code is best described as a sequence with: