Lesson 17.7 — CFG simplification and jump threading¶
Techniques: CFG simplification (constant-branch folding, unreachable-block elimination, block merging, empty-block removal, speculation into
select; LLVM's SimplifyCFG, the Clean algorithm of Cooper–Torczon); switch-to-lookup-table conversion; jump threading (Mueller–Whalley 1995; LLVM's JumpThreading with LazyValueInfo; GCC's backward threader) · Pebble implements:pebble-simplifycfg(exercise E4): constant branches, unreachable blocks, block merging · Prerequisites: Lesson 17.1 (SCCP leaves constant branches behind), Lesson 17.2 (LVI) · Time: 4–5 hours
1. Problem and motivation¶
Every optimization in this chapter leaves the control-flow graph messier than it found it: SCCP turns conditions into constants, ADCE turns branches into jumps and leaves empty blocks, GVN and PRE split edges. CFG simplification repairs this, and in doing so it enables the next round of dataflow analyses (fewer blocks, fewer joins, longer straight-line code for local optimizations). Jump threading goes one step further: when the outcome of a branch is known on some incoming edge, it duplicates the code in between so that edge jumps straight to the right successor.
CFG simplification¶
The classic formulation is Cooper and Torczon's Clean: fold a branch whose two targets are the same block, remove empty blocks, merge a block into its only predecessor, hoist a branch into an empty predecessor [EaC3, Ch. 10]; iterated to a fixed point it gives a canonical CFG. LLVM's SimplifyCFG does these and dozens more rewrites (sinking common code, turning two-entry phis into select, merging conditional branches) [LLVM-SimplifyCFG]; it is LLVM's most frequently run pass (8 instances in LLVM 23's default<O2> pipeline). pebble-simplifycfg implements the three core rewrites: constant branches, unreachable blocks, block merging.
Switch-to-lookup-table¶
A switch whose cases only select constants is a function from a dense range of integers to constants; it can be replaced by a bounds check and a load from a constant table. SimplifyCFG does this late in the pipeline, when the target can say whether a table is profitable (simplifySwitchLookup) [LLVM-SimplifyCFG].
Jump threading¶
Mueller and Whalley introduced "avoiding conditional branches by code replication": if a block \(B\) ends in a branch whose condition is known when \(B\) is entered from predecessor \(P\), redirect \(P\) to a copy of \(B\) that jumps directly (Mueller–Whalley, PLDI 1995; cited here through its production descendants). LLVM's JumpThreading asks LazyValueInfo (Lesson 17.2) for the value of the condition per predecessor and threads when the duplicated code is small (jump-threading-threshold, default 6 instructions) [LLVM-JumpThreading]; GCC threads in several places, most generally in its backward threader [GCC-TreeSSAPasses].
2. Definitions and algorithms¶
Definition 17.7.1 (CFG rewrites)
Let \(F\) be an SSA function. The core rewrites are:
(R1) constant branch: a terminator br i1 c, T, F with constant \(c\), br c, T, T, or switch on a
constant becomes br S for the selected successor \(S\); every other successor \(X\) loses the phi entries for
this block.
(R2) unreachable block: a block not reachable from the entry is deleted; its successors lose their phi
entries for it; its values' remaining uses (only in other unreachable blocks, or in phis of such edges) are
replaced by poison.
(R3) block merge: if \(B \neq \mathrm{entry}\) has exactly one predecessor \(P\), \(P\) ends in br B, and
\(B \neq P\), then \(B\)'s phis (one entry each) are replaced by their incoming values, \(P\)'s terminator is removed
and \(B\)'s instructions are appended to \(P\); successors' phis naming \(B\) now name \(P\).
(R4) empty block: a block containing only br S, whose removal does not make a phi of \(S\) ambiguous, is
removed by redirecting its predecessors to \(S\).
Rewrites on @cleanup
In the §3 example br i1 true, label %t, label %f is R1; %island (no predecessors) is R2; %t → %mid
→ %join become one block by R3.
Algorithm 17.7.3 (SimplifyCFG-lite)
- Input: an SSA function.
- Output: the function after R1–R3 applied to a fixed point.
- Precondition: the function verifies.
- Postcondition: no terminator has a constant condition or equal targets; every block is reachable; no block has a single predecessor ending in an unconditional branch to it (except the entry); behavior unchanged (Theorem 17.7.8).
- Invariant: the function verifies after every rewrite; the measure \(\lvert N \rvert + \lvert E \rvert + \lvert C \rvert\) (blocks, edges and conditional terminators) never increases, and each round that changes something decreases it (Theorem 17.7.9).
function SimplifyCFGLite(F):
repeat:
changed ← false
for each block B: # R1
if B's terminator has a constant condition or equal targets:
S ← selected successor; for each other successor X: X.removePredecessor(B)
replace the terminator by `br S`; changed ← true
R ← blocks reachable from the entry # R2
for each block B ∉ R:
for each successor X ∈ R: X.removePredecessor(B)
replace remaining uses of B's values by poison; delete B; changed ← true
for each block B ≠ entry: # R3
if B has exactly one predecessor P ≠ B and P ends in `br B`:
replace each phi of B by its only incoming value
rename B to P in the phis of B's successors
delete P's terminator; move B's instructions to the end of P; delete B; changed ← true
until not changed
Definition 17.7.4 (Table-convertible switch)
A switch on an integer \(x\) with cases \(c_1 < \dots < c_k\) and a default is table-convertible if every
case destination (and the default, or the default is unreachable) leads without side effects to one common
block \(J\) where phis select, for each case, a constant \(r_i\). Its table is the array
\(T[c - c_1] = r_i\) for \(c = c_i\), filled with the default's constant (or holes) for the other \(c\) in
\([c_1, c_k]\); its size is \(c_k - c_1 + 1\).
Algorithm 17.7.5 (Switch to lookup table)
- Input: a table-convertible
switch(Definition 17.7.4). - Output: a range check, a load from a constant global table, and a phi at \(J\).
- Precondition: the table's size is at most a threshold relative to the number of cases (density), and the target considers the table profitable.
- Postcondition: the new code yields \(r_i\) for \(x = c_i\) and the default result otherwise (Theorem 17.7.10).
- Invariant: none (a single rewrite).
Definition 17.7.6 (Threadable edge)
Let block \(B\) end in br c, T, F (or a switch). An incoming edge \((P, B)\) is threadable if the value
of \(c\) is known to be a constant \(k\) whenever control enters \(B\) from \(P\) — because \(c\) is a phi of \(B\) with a
constant incoming value for \(P\), or an expression over such phis, or because LVI proves \(c = k\) on the edge.
The threading target is the successor \(S_k\) that \(k\) selects.
Algorithm 17.7.7 (Jump threading)
- Input: an SSA function; LazyValueInfo (or phi constants) to evaluate conditions per edge.
- Output: the function with threadable edges redirected to copies of \(B\) that jump to \(S_k\).
- Precondition: \(B\) is not a loop header whose threading would create irreducible control flow (unless allowed); the number of instructions duplicated is below a threshold.
- Postcondition: the function computes the same results (Theorem 17.7.11); the branch in the copy is gone.
- Invariant: after each threading step, SSA form is valid (values defined in \(B\) and used after it get phis in their use blocks).
function JumpThread(F):
repeat until no change:
for each block B ending in a conditional branch or switch on c:
partition B's predecessors by the known value of c on the edge (or unknown)
for each group G of predecessors with known value k (and G ≠ all predecessors):
if cost(B) > threshold: skip
B' ← copy of B without the phis; its phis' values for G become plain values
B' ends in `br S_k`; redirect every P ∈ G from B to B'
in S_k and the other successors: add phi entries for B'
repair SSA: every value of B used outside B now gets phis merging it with its copy in B'
3. Worked example¶
CFG simplification¶
Algorithm 17.7.3 on @cleanup (the §7 box; pebble-simplifycfg produces the same final function as LLVM's simplifycfg):
entry: br i1 true, label %t, label %f
t: %x = add i32 %a, 1; br label %mid
mid: %y = mul i32 %x, 3; br label %join
f: %z = sub i32 %a, 1; br label %join
island: br label %join ; no predecessors
join: %p = phi i32 [ %y, %mid ], [ %z, %f ], [ 0, %island ]; ret i32 %p
| round | rule | change | blocks + edges |
|---|---|---|---|
| — | start | 6 + 6 = 12 | |
| 1 | R1 | entry: br i1 true → br label %t; f loses entry as a predecessor |
6 + 5 |
| 1 | R2 | reachable: entry, t, mid, join; delete f and island, join's phi drops their entries: %p = phi [%y, %mid] |
4 + 3 |
| 1 | R3 | t has one predecessor entry ending in br t: merge into entry; then mid (its predecessor is now entry); then join: its phi %p is replaced by %y |
1 + 0 |
| 2 | — | nothing applies: fixed point | 1 + 0 |
Result: entry: %x = add i32 %a, 1; %y = mul i32 %x, 3; ret i32 %y (§7 box).
Switch-to-lookup-table¶
days of the §7 box: 12 cases \(1 \dots 12\) with constant results, default 0. Table size \(12 - 1 + 1 = 12\) (fully dense), results fit in i8, so LLVM builds @switch.table.days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as [12 x i8]: idx = month − 1; idx <u 12 selects the table load, otherwise 0. The 13-way branch becomes one comparison and one load.
Jump threading¶
classify of the §7 box after mem2reg:
entry: %cmp = icmp slt i32 %x, 0; br i1 %cmp, label %if.then, label %if.else
if.then: br label %if.end
if.else: br label %if.end
if.end: %neg.0 = phi i32 [ 1, %if.then ], [ 0, %if.else ]
%mul = mul nsw i32 %x, 2; store i32 %mul, ptr %out
%tobool = icmp ne i32 %neg.0, 0; br i1 %tobool, label %if.then1, label %if.end2
| step | action |
|---|---|
| 1 | \(B\) = if.end, condition %tobool = icmp ne %neg.0, 0. From if.then: %neg.0 = 1, so %tobool = true → target if.then1. From if.else: %neg.0 = 0 → false → if.end2. Both predecessor groups are known. |
| 2 | cost of if.end: mul, store (the phi and the compare vanish in copies) — below the threshold 6: thread. |
| 3 | copy if.end for if.then (it becomes part of the path to if.then1) and for if.else (to if.end2); the mul and the store are duplicated (%mul2 in the box). |
| 4 | the now empty if.then/if.else and the original if.end are cleaned up: entry branches on %cmp directly to the two copies. |
The dynamic path has one conditional branch instead of two, at the price of one duplicated mul + store.
4. Invariants and correctness¶
Theorem 17.7.8 (R1–R4 preserve behavior)
Each rewrite of Definition 17.7.1 transforms a verifying SSA function into one that verifies and has the same observable behavior on every input.
Proof
R1. The removed edges are never taken: the condition is a constant (or both targets coincide), so every
execution of \(B\) continues to \(S\). Phi entries for removed edges were never selected. SSA stays valid: the
remaining edge set is a subset, so dominance only grows (every path in the new CFG is a path in the old).
R2. An unreachable block never executes; removing it removes only never-selected phi entries. Uses of its
values outside it can only be in blocks it dominates (strict SSA), which are unreachable too, or in phi
entries for its outgoing edges, which were removed; any remaining use is in dead code and poison is a
refinement there. R3. Every execution of \(B\) comes from \(P\) through the unconditional branch, and every
execution of \(P\) continues into \(B\): concatenating the instruction sequences executes the same instructions
in the same order. \(B\)'s phis have one entry (single predecessor), so they equal that value. Uses of \(B\)'s
values remain dominated: \(P\) dominated everything \(B\) dominated. R4. Redirecting \(B\)'s predecessors to
\(S\) skips only br S, provided each phi of \(S\) can take, for a redirected predecessor, the value it took for
\(B\) without conflicting with an existing entry from that predecessor — exactly the condition of R4.
Theorem 17.7.9 (SimplifyCFG-lite terminates at a fixed point)
Let \(C\) be the set of conditional terminators (br i1 with two targets, and switch). Algorithm 17.7.3
terminates after at most \(\lvert N \rvert + \lvert E \rvert + \lvert C \rvert + 1\) rounds, and the result has
none of the patterns R1–R3 apply to.
Proof
The measure \(\mu = \lvert N \rvert + \lvert E \rvert + \lvert C \rvert\) is a natural number (edges counted with
multiplicity, one per successor slot). R1 replaces a conditional terminator by br, so \(\lvert C \rvert\) drops
by one (and \(\lvert E \rvert\) usually too; a switch with no cases is the one R1 that keeps its single
edge, which is why \(\lvert C \rvert\) is in the measure), R2 removes at least one block, R3 removes one block
and one edge; none adds blocks, edges or conditional terminators. So every round that changes
anything decreases \(\mu\) by at least 1, and the final round, which changes nothing, certifies that no rule
applies to any block.
Theorem 17.7.10 (Lookup tables compute the switch)
Under Definition 17.7.4 and Algorithm 17.7.5, the value at \(J\) equals the value the phi would have selected, for every \(x\).
Proof
Case \(x = c_i\): \(\mathit{idx} = c_i - c_1 \in [0, \mathit{size})\) as an unsigned number, so the table branch is taken and \(T[c_i - c_1] = r_i\) by construction. Case \(x \in [c_1, c_k]\) not a case: the switch takes the default, the table holds the default's constant there. Case \(x \notin [c_1, c_k]\): work modulo \(2^w\), where \(w\) is the bit width. The map \(x \mapsto (x - c_1) \bmod 2^w\) is a bijection of the \(2^w\) bit patterns, and it sends the \(\mathit{size}\) patterns of \([c_1, c_k]\) exactly onto \(\{0, \dots, \mathit{size} - 1\}\). Every other \(x\) therefore lands on an unsigned value \(\geq \mathit{size}\), the check fails and the default is taken. The case blocks had no side effects (Definition 17.7.4), so removing them changes nothing else.
Theorem 17.7.11 (Jump threading preserves behavior)
If the edge value used in Algorithm 17.7.7 is sound (the condition equals \(k\) whenever \(B\) is entered from a predecessor in \(G\)), then after threading every execution performs the same side effects and returns the same value.
Proof
Consider an execution entering \(B\) from \(P \in G\). In the old program it executes \(B\)'s instructions with the phis set to their \(P\) entries, evaluates the condition to \(k\) (soundness) and goes to \(S_k\). In the new program it enters \(B'\), which executes the same instructions with the phis' \(P\) values substituted, and goes to \(S_k\) unconditionally: the same side effects and values. Executions entering from other predecessors see the unchanged \(B\). At \(S_k\) and at blocks after it, values defined in \(B\) now have two definitions (in \(B\) and \(B'\)); the SSA repair step inserts phis selecting the copy by the path taken, so every use reads the value of the block that actually executed — the value the old program had. Successor phis get entries for \(B'\) equal to their entries for \(B\) (with \(B\)'s values replaced by their copies).
What breaks it. Threading through a loop header can turn a natural loop into an irreducible region (two entries), which is why LLVM does not thread across loop headers by default (jump-threading-across-loop-headers=false). Speculating a block into a select (SimplifyCFG's foldTwoEntryPHINode) must not execute instructions that can trap or have side effects on the path that did not execute them.
5. Complexity¶
\(n\) blocks, \(e\) edges, \(I\) instructions, \(t\) the jump-threading duplication threshold.
| Technique | Time (worst) | Time (typical) | Space | Justification |
|---|---|---|---|---|
| SimplifyCFG-lite | \(O((n + e) \cdot (n + e + I))\): at most \(2n + e + 1\) rounds (\(\lvert C \rvert \leq n\)), each linear | 2–3 rounds | \(O(n + e)\) | Theorem 17.7.9; each round visits every block once and the reachability walk is linear |
| Switch to lookup table | \(O(k \log k + \mathit{size})\) for \(k\) cases | linear | \(O(\mathit{size})\) table | sort cases, fill the table |
| Jump threading | each threading step duplicates at most \(t\) instructions; repeated to a fixed point | few steps per function | code growth up to \(t\) per threaded edge | LVI queries dominate (Lesson 17.2) |
Pathological family for jump threading. A chain of \(m\) tests of the same flag, each followed by a small block: if (f) A1; if (f) A2; …; if (f) Am. Each join's branch is known from both predecessors, so threading can turn the chain into two straight paths — but every threading step duplicates the blocks between tests, and without the threshold the code can grow by a factor of 2 per unknown flag when different flags interleave (\(2^m\) paths for \(m\) independent flags tested twice each). The per-block threshold (6 instructions) and the requirement that a whole predecessor group have a known value keep the growth linear in practice. At scale: SimplifyCFG appears 8 times and jump threading twice in LLVM 23's default<O2> pipeline (opt -passes='default<O2>' -print-pipeline-passes).
6. Variants and refinements¶
- Speculation into select — a two-entry phi whose arms are cheap and safe becomes a
select(thesimplifycfgoutput in the §7 box of jump threading); trade-off: executes both arms. - Sinking and hoisting common code — SimplifyCFG's
sinkCommonCodeFromPredecessors, GVN-sink (Lesson 17.5); trade-off: shorter code, fewer scheduling choices. - Switch lowering alternatives — lookup tables, bit tests, jump tables, binary search trees (SelectionDAG's switch lowering, Ch 11); trade-off: memory vs branches.
- DFA jump threading — threads across loop back edges for state machines (
dfa-jump-threadingin LLVM's-O2), duplicating whole paths; trade-off: large code growth for big speedups on interpreters and lexers. - Path duplication and superblocks — more aggressive tail duplication (Ch 23).
- Structured simplification (MLIR) — region-based canonicalization of
scfops rather than CFG edits.
7. In real compilers¶
CFG simplification¶
LLVM: llvm/lib/Transforms/Utils/SimplifyCFG.cpp — SimplifyCFGOpt::simplifyOnce removes blocks without predecessors, calls ConstantFoldTerminator, MergeBlockIntoPredecessor, foldTwoEntryPHINode, foldBranchToCommonDest and many more; llvm/lib/Transforms/Scalar/SimplifyCFGPass.cpp iterates to a fixed point [LLVM-SimplifyCFG]. GCC's cleanup_tree_cfg (tree-cfgcleanup.cc) does the same after every GIMPLE pass that changes the CFG.
LLVM simplifycfg and pebble-simplifycfg on @cleanup
Reproduce (opt 23.1.2; PebblePasses.so from a solution build of the course, build/<dir>/lib/):
cat > cfg.ll <<'EOF'
define i32 @cleanup(i32 %a) {
entry:
br i1 true, label %t, label %f ; constant branch
t:
%x = add i32 %a, 1
br label %mid ; mid has one predecessor: merge
mid:
%y = mul i32 %x, 3
br label %join
f:
%z = sub i32 %a, 1
br label %join
island: ; no predecessors
br label %join
join:
%p = phi i32 [ %y, %mid ], [ %z, %f ], [ 0, %island ]
ret i32 %p
}
EOF
opt -passes=simplifycfg -S cfg.ll | sed -n '/^define/,/^}/p'
Output:
What to notice: the §3 trace's result: R1 folded br i1 true, R2 deleted %f and %island (and the
phi entries), R3 merged the chain into entry, and the phi with one entry became %y.
opt -load-pass-plugin=<build>/lib/PebblePasses.so -passes=pebble-simplifycfg prints the same function.
Switch-to-lookup-table¶
LLVM: simplifySwitchLookup and shouldBuildLookupTable in llvm/lib/Transforms/Utils/SimplifyCFG.cpp, enabled by the switch-to-lookup option of simplifycfg (set late in the -O2 pipeline) [LLVM-SimplifyCFG].
A switch becomes a table
Reproduce (clang 23.1.2, opt 23.1.2):
cat > sw.c <<'EOF'
int days(int month) {
switch (month) {
case 1: return 31; case 2: return 28; case 3: return 31; case 4: return 30;
case 5: return 31; case 6: return 30; case 7: return 31; case 8: return 31;
case 9: return 30; case 10: return 31; case 11: return 30; case 12: return 31;
default: return 0;
}
}
EOF
clang-23 -O0 -Xclang -disable-O0-optnone -fno-discard-value-names -S -emit-llvm sw.c -o sw.O0.ll
opt -passes=mem2reg -S sw.O0.ll -o sw.ll
opt -passes='simplifycfg<switch-to-lookup>' -S sw.ll | sed -n '/^@switch/p;/^define/,/^}/p'
Output:
@switch.table.days = private unnamed_addr constant [12 x i8] c"\1F\1C\1F\1E\1F\1E\1F\1F\1E\1F\1E\1F", align 4
define dso_local i32 @days(i32 noundef %month) #0 {
entry:
%switch.tableidx = sub i32 %month, 1
%0 = icmp ult i32 %switch.tableidx, 12
br i1 %0, label %switch.lookup, label %return
switch.lookup: ; preds = %entry
%1 = zext nneg i32 %switch.tableidx to i64
%switch.gep = getelementptr inbounds [12 x i8], ptr @switch.table.days, i64 0, i64 %1
%switch.load = load i8, ptr %switch.gep, align 1
%switch.ext = zext i8 %switch.load to i32
br label %return
return: ; preds = %entry, %switch.lookup
%retval.0 = phi i32 [ %switch.ext, %switch.lookup ], [ 0, %entry ]
ret i32 %retval.0
}
What to notice: Algorithm 17.7.5 exactly: idx = month − 1, the unsigned range check idx <u 12
(Theorem 17.7.10's argument for out-of-range values), and a table of i8 (the results fit) —
\1F is 31, \1C is 28, \1E is 30.
Jump threading¶
LLVM: llvm/lib/Transforms/Scalar/JumpThreading.cpp — JumpThreadingPass::processThreadableEdges groups predecessors by computeValueKnownInPredecessors (which asks LVI), threadEdge duplicates the block and repairs SSA; the jump-threading-threshold option bounds the duplicated size [LLVM-JumpThreading]. GCC: gcc/tree-ssa-threadbackward.cc, class back_threader, which searches backwards from a branch for paths on which its condition is constant, using Ranger.
jump-threading duplicates a block to skip a known branch
Reproduce (clang 23.1.2, opt 23.1.2):
cat > jt.c <<'EOF'
int classify(int x, int *out) {
int neg;
if (x < 0)
neg = 1;
else
neg = 0;
*out = x * 2; // work between the two tests
if (neg) // the same decision again: known on each incoming edge
return -1;
return 1;
}
EOF
clang-23 -O0 -Xclang -disable-O0-optnone -fno-discard-value-names -S -emit-llvm jt.c -o jt.O0.ll
opt -passes=mem2reg -S jt.O0.ll -o jt.ll
opt -passes=jump-threading -S jt.ll | sed -n '/^define/,/^}/p'
Output:
define dso_local i32 @classify(i32 noundef %x, ptr noundef %out) #0 {
entry:
%cmp = icmp slt i32 %x, 0
br i1 %cmp, label %if.then1, label %if.end2
if.then1: ; preds = %entry
%mul2 = mul nsw i32 %x, 2
store i32 %mul2, ptr %out, align 4
br label %return
if.end2: ; preds = %entry
%mul = mul nsw i32 %x, 2
store i32 %mul, ptr %out, align 4
br label %return
return: ; preds = %if.end2, %if.then1
%retval.0 = phi i32 [ -1, %if.then1 ], [ 1, %if.end2 ]
ret i32 %retval.0
}
What to notice: the §3 trace: the second test is gone, entry branches straight to the two outcomes,
and the mul + store of if.end were duplicated (%mul2). (Running simplifycfg instead turns the
same function into two selects — speculation, §6 — with no duplication and no branch at all.)
Find where LLVM does it. In llvm/lib/Transforms/Scalar/JumpThreading.cpp, what is the name and default of the command-line option that limits how many instructions a block may have to be duplicated? (Quiz llvm-where-jt-threshold.)
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| CFG simplification | Canonical CFG: no constant branches, no unreachable blocks, no mergeable chains | rounds × linear · cheap, run often | Fewer blocks and edges; selects |
Low for the core rules, high for LLVM's full set | After every CFG-changing pass (LLVM 8× at -O2) |
| Switch-to-lookup-table | Dense constant switches only | \(O(k \log k + \mathit{size})\) | A constant table + range check | Medium (profitability, holes, types) | Late SimplifyCFG in LLVM |
| Jump threading | Removes branches whose outcome is known per edge (with LVI: ranges, predicates) | LVI queries + bounded duplication | Duplicated blocks | Medium–high (SSA repair, loop headers) | LLVM jump-threading 2× at -O2; GCC threaders |
Choose CFG simplification after any pass that folds conditions or deletes code: it is cheap and makes every later analysis faster. Choose lookup tables when a switch maps a dense range to constants and the target loads cheaply. Choose jump threading when correlated branches (the same flag or range tested twice) sit on hot paths and the duplicated code is small.
9. Assessment¶
- Quiz:
simplifycfg-rounds,simplifycfg-merge(tagsimplifycfg);lookup-table-size,lookup-range-check(tagswitch-lookup);jt-known-edge,llvm-where-jt-threshold(tagjump-threading). - Drill: none of this chapter's drills generates CFG-rewrite problems: the rewrites are local and their outcomes are checked by the E4 tests on concrete IR; the quiz asks for the rounds, the table and the threaded edges on given functions.
- Flashcards: tags
simplifycfg,switch-lookup,jump-threading. - Exercises: E4
pebble-simplifycfg.
References¶
See the chapter references.