Basic Blocks and DAG-Based Optimization for Intermediate Code

Basic Blocks and DAG-Based Optimization for Intermediate Code

Verified Sources
Nov 4, 2026

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:

  1. 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 x with that operator node (the “value” computed).
  2. For assignments x = y:
    • create a copy relation (alias) from x to the same DAG node as y.

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

  1. 1
    Step 1

    Given TAC: (1) d=b∗cd=b*c, (2) e=a+be=a+b, (3) b=b∗cb=b*c, (4) a=e−da=e-d. Expressions are (b∗c)(b*c), (a+b)(a+b), and (e−d)(e-d); note the redefinition of bb affects which (b∗c)(b*c) refers to.

  2. 2
    Step 2

    Create leaves (variable nodes) for a,b,ca, b, c. Each operator node will reference the current DAG value nodes corresponding to the operands.

  3. 3
    Step 3

    Create operator node nbcn_{bc} for (\*,b,c)(\* , b, c). Label dd to point to nbcn_{bc}. The available value of bb used here is the original bb (before statement 3).

  4. 4
    Step 4

    Create operator node nabn_{ab} for (+,a,b)(+ , a, b). Label ee to point to nabn_{ab}. Here bb is still the original bb (since statement 3 hasn’t happened).

  5. 5
    Step 5

    This recomputes b∗cb*c but with bb updated on the left. Since the right side uses the current bb value (which is still the original bb at this point), the expression (b∗c)(b*c) is equivalent to the earlier one in this block; therefore reuse nbcn_{bc} and relabel the variable bb to point to nbcn_{bc} again.

  6. 6
    Step 6

    Create operator node nedn_{ed} for (−,e,d)(- , e, d). Label aa to point to nedn_{ed}. Also note ee and dd refer to their labeled nodes from earlier: e→nabe \to n_{ab} and d→nbcd \to n_{bc}.

  7. 7
    Step 7

    Operator nodes: nbcn_{bc} for (b∗c)(b*c), nabn_{ab} for (a+b)(a+b), nedn_{ed} for (e−d)(e-d). Variable labels at the end: a→neda \to n_{ed}; b→nbcb \to n_{bc} (though bb is overwritten later only here); cc remains a leaf input; dd and ee 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 (b∗c)(b*c) 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 a=e−da = e - d; therefore the computation depends on (a+b)(a+b) and (b∗c)(b*c).

Optimize the three-address code assuming only $a$ is live on exit from the block

  1. 1
    Step 1

    Given: only aa is live on exit. Therefore, any computation whose result is not needed to compute the final live aa can be removed.

  2. 2
    Step 2

    Statement (4) computes aa from ee and dd. So we must know how ee and dd are obtained: ee comes from (2), and dd comes from (1).

  3. 3
    Step 3

    Statement (3) reassigns b=b∗cb=b*c, but statement (4) uses dd (not bb). Since dd was computed at (1) and is not overwritten, statement (3) does not affect the value of aa.

  4. 4
    Step 4

    Drop statement (3) because its result (bb after stmt3) is never used to compute the final live variable aa.

  5. 5
    Step 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.

  6. 6
    Step 6

    The optimized code is: d=b∗cd = b*c e=a+be = a+b a=e−da = e-d

How DAG reuses expressions correctly in a block

When an expression like (b∗c)(b*c) 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, bb is not changed until the left side of statement (3), so the right side of (3) uses the same bb 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, bb is redefined at (3), yet the final live variable aa depends only on dd and ee, 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

1

Partition TAC into leaders/basic blocks (single entry/exit)."

Build expression DAG

2

Create operator nodes for expressions and label variables to those nodes."

Mark needed values via liveness

3

With only aa live on exit, mark DAG nodes needed to compute aa."

Regenerate optimized TAC

4

Emit only the statements producing marked nodes, using DAG reuse to avoid duplicates."

Knowledge Check

Question 1 of 3
Q1Single choice

A basic block in intermediate code is best described as a sequence with: