Lesson 21.6 — GlobalISel and FastISel¶
Techniques: FastISel; the GlobalISel pipeline (IRTranslator, Legalizer, RegBankSelect, InstructionSelect); GlobalISel combiners · Lab:
labs/ch21-mir(tasks 3, 4, 8) · Prerequisites: Lesson 21.5 (SelectionDAG, MIR) · Time: 4–5 hours
SelectionDAG is thorough but slow, and it sees one basic block at a time. LLVM has two other selectors that trade differently. FastISel is built for -O0: it walks the IR once and emits machine instructions directly, one IR instruction at a time. It handles the common cases and hands anything harder to SelectionDAG. GlobalISel replaces the DAG with generic MIR: machine instructions with target-independent opcodes (G_ADD, G_LOAD) over typed virtual registers. It lowers them in four passes (translate, legalize, choose register banks, select) that run on the whole function, and combiners between them do the simplification work. On AArch64, GlobalISel is the default at -O0, and it selects a[i] = p[3] into the same two instructions SelectionDAG does. On x86-64 it is still incomplete, and there it produces four instructions for the same input.
1. Problem and motivation¶
The problem is the same as in Lesson 21.5: LLVM IR in, SSA-form MIR out. The constraints differ. At -O0 the goal is compile speed and debuggability: selection should be close to linear with a small constant and keep IR instructions recognizable. For optimized builds, SelectionDAG's per-block view blocks selection across blocks (folding a compare into a branch in another block, choosing register banks with a global view), and building and throwing away a DAG per block is expensive [LLVM-GISel].
FastISel¶
FastISel appeared in LLVM 2.x (2008) as a fast path for -O0. It selects most instructions by table lookup on (IR opcode, type), with small hand-written target hooks for loads, stores, calls and addresses (X86FastISel, AArch64FastISel). It falls back to SelectionDAG for anything it does not handle. In the terms of Lesson 21.1, it is macro expansion with a few local folds [LLVM-FastISel].
GlobalISel pipeline¶
GlobalISel was proposed in 2015 (Quentin Colombet's design at the LLVM Developers' Meeting) and became AArch64's -O0 default in LLVM 7. Its goals, stated in the documentation, are performance (faster than SelectionDAG), granularity (the whole function as the unit, not a block), and modularity (separate, reusable passes that can be tested one at a time on MIR). It reuses SelectionDAG's TableGen patterns for its final selection step [LLVM-GISel, Col25, Ch. 14–17].
GlobalISel combiners¶
The DAG combiner's work (canonicalize, fold, simplify) is done in GlobalISel by combiner passes run before and after legalization. Their rules are written as TableGen GICombineRules with a match part (MIR patterns plus C++ predicates) and an apply part [LLVM-GISel-Combine]. Because they operate on MIR with use-def chains across the whole function, they can combine across block boundaries.
2. Definitions and algorithms¶
Definition 21.6.1 (Generic MIR, LLTs, register banks)
Generic MIR is MIR whose instructions may use target-independent generic opcodes
(G_ADD, G_PTR_ADD, G_LOAD, G_CONSTANT, …, listed in
llvm/include/llvm/Target/GenericOpcodes.td) and whose virtual registers carry a
low-level type (LLT) instead of a register class: a scalar of \(n\) bits (printed i64 or
s64), a pointer in address space \(a\) (p0), or a vector. A register bank is a set of
register classes that can hold the same values without copies (AArch64: gpr and fpr).
During selection a virtual register goes from %5:_(i64) (type only) to %5:gpr(i64) (type
and bank) to %5:gpr64 (register class, generic type gone). An instruction is selected
when its opcode is a target opcode.
Definition 21.6.2 (Legalizer rule table)
For each generic opcode and each type index, a target declares an ordered list of rules
\((\text{predicate on types}, \text{action})\), with actions Legal, NarrowScalar,
WidenScalar, FewerElements, MoreElements, Bitcast, Lower, Libcall, Custom or
Unsupported. The action of an instruction is that of the first rule whose predicate
holds for its types. Example (AArch64, getActionDefinitionsBuilder({G_ADD, G_SUB, …})):
.legalFor({i32, i64, v8i8, …}).widenScalarToNextPow2(0).clampScalar(0, s32, s64). So G_ADD
on i32 or i64 is legal, i8 is widened to i32, and i128 is narrowed.
FastISel¶
Algorithm 21.6.3 (FastISel with fallback)
- Input: a basic block of LLVM IR; the target's FastISel hooks.
- Output: machine instructions for the block.
- Precondition: the function's arguments and the block's PHI inputs could be lowered (otherwise the whole function falls back).
- Postcondition: every instruction is selected, by FastISel or by SelectionDAG (Proposition 21.6.4).
- Invariant: the instructions after the current position (the block is walked bottom-up)
are selected. Values defined in the block have virtual registers in
ValueMap.
function FastSelectBlock(block):
BI ← end(block)
while BI ≠ begin(block):
I ← instruction before BI
if I is dead or was folded into a later instruction: BI ← BI − 1; continue
if SelectInstruction(I): # table lookup + target hooks
BI ← BI − 1
if the instruction before I is a load with one use and
target's TryToFoldLoad(load, I) succeeds: # e.g. into ADD64rm
skip the load
continue
if I is a call: select I alone with SelectionDAG; BI ← BI − 1; continue
select [begin(block), BI) with SelectionDAG # hand over the rest
return
function SelectInstruction(I):
if I is a terminator: first emit copies for successor PHIs, or fail
return SelectOperator(I) or target's FastSelectInstruction(I)
Proposition 21.6.4 (FastISel with fallback is complete and correct)
If every target hook emits code equivalent to the instruction it selects, then Algorithm 21.6.3 produces a correct selection for every block that SelectionDAG can select.
Proof
Each instruction is handled by exactly one of three paths: FastISel (correct by the
hypothesis on hooks), a single-instruction SelectionDAG block for a call, or SelectionDAG for
the remaining prefix. SelectionDAG is correct and complete for the prefix whenever it can
select the block at all. The values that cross between the parts go through virtual registers
recorded in ValueMap, which both selectors read and write, so the parts compose like
separately selected blocks. Folding a load into its single user is correct because the load
is immediately before the user (no intervening store) and has no other use.
GlobalISel pipeline¶
Algorithm 21.6.5 (The GlobalISel pipeline)
- Input: a function in LLVM IR; the target's
CallLowering,LegalizerInfo,RegisterBankInfoandInstructionSelector. - Output: selected SSA MIR (no generic opcodes, register classes on all virtual registers).
- Precondition: the target implements the four components for the constructs in the
function (otherwise
-global-isel-abortdecides between an error and a fallback to SelectionDAG). - Postcondition: after each pass its invariant holds for the whole function: translated → legal → banked → selected (Theorem 21.6.6).
- Invariant: each pass only moves instructions forward along that order and verifies its postcondition (the MachineVerifier checks it in assertion builds).
function GlobalISel(F):
IRTranslator(F) # IR → generic MIR, one IR instruction at a time;
# calls and arguments through CallLowering (the ABI)
PreLegalizerCombiner(F) # optional: canonicalize (mul by 2^k → shl, ...)
Legalizer(F)
PostLegalizerCombiner(F) # optional
RegBankSelect(F)
InstructionSelect(F)
function Legalizer(F):
W ← all instructions
while W not empty:
MI ← pop(W)
switch Action(MI): # Definition 21.6.2
Legal: continue
NarrowScalar(T'): split each value into pieces of type T' (G_UNMERGE_VALUES),
operate on the pieces (G_ADD → G_UADDO + G_UADDE),
rebuild (G_MERGE_VALUES)
WidenScalar(T'): G_ANYEXT/G_SEXT/G_ZEXT the operands to T', operate, G_TRUNC
Lower: rewrite in terms of other generic instructions
Libcall: emit a call; Custom: target's legalizeCustom(MI)
push the new instructions onto W
ArtifactCombine: fold G_TRUNC(G_ANYEXT x) → x, G_UNMERGE(G_MERGE ...) → ..., and so on
function RegBankSelect(F):
for each instruction MI (in a reverse post-order of the blocks):
M ← the target's cheapest instruction mapping for MI (a bank per operand), given
the banks already assigned to its operands (Greedy mode: compare several)
for each operand whose assigned bank differs from M: insert a repair COPY
assign M's banks to MI's definitions
function InstructionSelect(F):
for each block, for each instruction MI from last to first:
if MI is generic:
select(MI) via the TableGen-imported patterns (GIMatchTable), which may fold
single-use generic definitions (e.g. G_PTR_ADD, G_SHL) into MI's operands,
or via the target's C++ select(); on failure: fall back or abort
Theorem 21.6.6 (Legalizer termination and the pipeline's guarantees)
(a) If every non-Legal action produces instructions that are strictly smaller in a
well-founded order (narrowing and widening move types towards the legal set, lowerings use
"simpler" opcodes, as in Theorem 21.5.9), the Legalizer terminates, and afterwards every
instruction is Legal. (b) After RegBankSelect every virtual register has exactly one bank,
and every instruction's operands are in banks its mapping accepts. (c) After InstructionSelect
no generic opcode remains, or the function was sent to the fallback.
Proof
(a) As in Theorem 21.5.9: the multiset of (rank of each non-legal instruction) decreases
under the multiset ordering at every step, and the artifact combines only delete instructions
or replace a pair by a simpler one. The loop ends only with an empty worklist, and every
popped instruction was either legal or replaced. (b) Each definition is assigned the bank of
the chosen mapping exactly once, because instructions are visited once and each virtual
register has one definition (SSA). A use whose bank differs from what the mapping needs gets
a repair COPY into a new virtual register of the right bank, so every operand ends up in an
accepted bank. (c) InstructionSelect visits every instruction. For a generic one it either
replaces it with target instructions (a pattern or C++ select) or reports failure, which,
depending on -global-isel-abort, is an error or a fallback to SelectionDAG for the whole
function.
GlobalISel combiners¶
Definition 21.6.7 (GlobalISel combine rule)
A combine rule is a pair (match, apply): match is a MIR pattern rooted at a definition
((G_MUL $d, $op1, $op2)) together with a C++ predicate that may compute data (matchinfo),
and apply rewrites the matched instructions using that data. A combiner is a pass that
applies a set of rules to a worklist until no rule matches. Examples in
llvm/include/llvm/Target/GlobalISel/Combine.td: mul_to_shl (G_MUL x, 2^k →
G_SHL x, k), sub_to_add (G_SUB x, C → G_ADD x, −C).
Algorithm 21.6.8 (Combiner pass)
- Input: generic MIR; a rule set; a position in the pipeline (pre- or post-legalizer).
- Output: equivalent generic MIR.
- Precondition: each rule preserves semantics. After legalization, rules must produce legal instructions (the post-legalizer combiners check legality).
- Postcondition: no rule matches any instruction (a fixed point), as for Algorithm 21.5.4.
- Invariant: every instruction that may match is on the worklist. An observer adds created and changed instructions (and the users of changed registers) to it.
function Combiner(F, rules):
W ← all instructions, in a post-order of the blocks
repeat:
changed ← false
while W not empty:
MI ← pop(W)
if MI is trivially dead: erase MI; continue
for each rule r whose root opcode is opcode(MI), in priority order:
if r.match(MI, info): r.apply(MI, info); changed ← true; break
until not changed (or a pass-specific iteration limit is reached)
3. Worked example¶
FastISel¶
The sum loop of task 6 (labs/ch21-mir/inputs/sum.ll) at -O0. The loop block has phi, phi, getelementptr, load, add, add, icmp and br. FastISel walks it bottom-up:
| step | IR instruction | outcome |
|---|---|---|
| 1 | br i1 %c, … |
the target hook sees that %c is an icmp with one use and emits CMP64rr + JCC_1 together (the compare is marked folded) |
| 2 | %c = icmp slt |
skipped: folded in step 1 |
| 3 | %i1 = add i64 %i, 1 |
ADD64ri32 (immediate form, from the table) |
| 4 | %s1 = add i64 %s, %x |
selected as an add; the instruction before it is %x = load with one use, so tryToFoldLoad rewrites it to ADD64rm with the address |
| 5 | %x = load |
skipped: folded |
| 6 | %p = getelementptr |
skipped: folded into the address of step 4 |
| 7 | the two phis |
not selected by FastISel. PHIs are created from the IR phis, and each predecessor's terminator copies the incoming values |
The result has ADD64rm, ADD64ri32, CMP64rr and JCC_1 in the loop, one instruction per IR operation except where a local fold applies (the §7 box shows it).
GlobalISel pipeline¶
a[i] = p[3] on AArch64 with -global-isel (the outputs are in §7):
| after | the address a + 8·i |
the load | register classes |
|---|---|---|---|
| IRTranslator | %6 = G_CONSTANT 8; %7 = G_MUL %1, %6; %8 = G_PTR_ADD %0, %7 |
%5 = G_LOAD %4 with %4 = G_PTR_ADD %2, 24 |
_ (types only) |
| pre-legalizer combiner | %10 = G_CONSTANT 3; %7 = G_SHL %1, %10 (mul_to_shl) |
unchanged | _ |
| Legalizer | unchanged: everything is legal on i64/p0 |
unchanged | _ |
| RegBankSelect | all operands gpr |
gpr |
banks only |
| InstructionSelect | folded into STRXroX %5, %0, %1, 0, 1 (register offset, shift 3) |
LDRXui %2, 3 (unsigned offset 24/8) |
gpr64, gpr64sp |
InstructionSelect visits G_STORE first (bottom-up). Its imported pattern (store GPR64:$Rt, (ro_Xindexed64 GPR64sp:$Rn, GPR64:$Rm, ro_Xextend64:$extend)) matches through the single-use G_PTR_ADD and G_SHL, which then become dead.
Legalizer on wide.ll (AArch64):
| function | before (IRTranslator) | rule (Definition 21.6.2) | after |
|---|---|---|---|
add8 |
G_TRUNCs to i8, %4:_(i8) = G_ADD, G_ANYEXT to i32 |
widenScalarToNextPow2/clampScalar(0, s32, s64): widen to i32 |
%8:_(i32) = G_ADD %2, %3; the G_TRUNC/G_ANYEXT pairs are cancelled by the artifact combiner |
add128 |
G_MERGE_VALUES of two i64 halves, then G_ADD on i128 |
clampScalar: narrow to i64 |
%13, %19 = G_UADDO %2, %4 and %15, %18 = G_UADDE %3, %5, %19 (the carry %19 links them) |
GlobalISel combiners¶
mul_to_shl on the IRTranslator output above: the match pattern (G_MUL $d, $op1, $op2) matches %7 = G_MUL %1, %6, and matchCombineMulToShl checks that %6 is a constant power of two (8) and stores \(\log_2 8 = 3\) in matchinfo. The apply creates %10 = G_CONSTANT i64 3 and rewrites the instruction to %7 = G_SHL %1, %10. The old G_CONSTANT 8 becomes dead and is erased on its next pop. A second pass over the worklist finds no match: fixed point.
Try it
Tasks 3 and 4 of labs/ch21-mir walk the same pipeline with
-stop-after=irtranslator, legalizer, regbankselect and instruction-select, and
./course drill legalization --difficulty medium practices the actions GlobalISel's rule
tables express differently from SelectionDAG's.
4. Invariants and correctness¶
FastISel¶
Proposition 21.6.4. The subtle part is the bottom-up walk combined with local folding: when FastISel selects the add in step 4, it may fold the preceding load only if nothing between them could change memory. That holds because the load is immediately before the add. It also relies on the load having a single use, or the other user would read a value that no longer exists. tryToFoldLoad and the hasOneUse test in SelectAllBasicBlocks check both conditions.
GlobalISel pipeline¶
Theorem 21.6.6 gives each pass's postcondition. The artifact combiner is what makes the Legalizer's local rewrites compose: widening an i8 G_ADD inserts extends and truncates around it, and without cancelling them, a chain of i8 operations would carry an extend and a truncate at every step. The artifact combines are equalities (G_TRUNC(G_ANYEXT x) = x when the types match), so they preserve meaning.
GlobalISel combiners¶
The same argument as Proposition 21.5.5: each rule must preserve meaning, and the pass terminates only if the rules decrease a measure. GlobalISel combiners additionally cap the number of iterations over the function. A rule pair that fights still stops, just without reaching a fixed point.
5. Complexity¶
Variables: \(n\) IR instructions in the function, \(m\) generic MIR instructions (\(m = O(n)\) except for large expansions), \(R\) legalizer rules per opcode, \(b\) register banks.
| Technique | Time (worst) | Time (typical) | Space | Justification |
|---|---|---|---|---|
| FastISel | \(O(n)\) plus the fallback's cost | linear, small constant | \(O(n)\) value map | one table lookup or hook per instruction |
| GlobalISel pipeline | \(O(m \cdot R)\) legalizing plus \(O(m \cdot b)\) bank mapping plus selection | linear passes over the function | the MIR itself; no per-block DAG | each pass visits each instruction a bounded number of times (Theorem 21.6.6's measure) |
| GlobalISel combiners | unbounded without a measure; capped by an iteration limit | linear per iteration | worklist \(O(m)\) | as Algorithm 21.5.4 |
A pathological family for FastISel. A block whose first instruction is not supported (say, an unsupported intrinsic at the top) and whose other instructions are all simple. FastISel walks bottom-up, selects everything down to the failing instruction, and then hands only the prefix (one instruction) to SelectionDAG, which is cheap. A failing instruction at the bottom (an unsupported terminator) hands the entire block to SelectionDAG, and FastISel's work on the block is wasted. The cost of a block is between the two extremes, depending on where the first failure is.
Real-world scale. The GlobalISel documentation lists compile-time performance as a main goal and reports AArch64 -O0 as the production use that motivated the switch [LLVM-GISel]. On x86-64 in LLVM 23.1.2, GlobalISel still falls back for common operations: the §7 box shows it giving up on a 128-bit division and on ctpop.
6. Variants and refinements¶
FastISel¶
- Target hooks (
X86FastISel::X86SelectAddress,fastSelectInstruction): fold addresses and compares locally for better-O0code at low cost. - Fall back per instruction vs per block: calls are selected alone, everything else hands over the prefix. The trade-off is between code quality and FastISel's own complexity.
- Replace with GlobalISel at
-O0(AArch64 since LLVM 7): one selector for all levels instead of FastISel plus SelectionDAG. The price is the maturity of the GlobalISel port.
GlobalISel pipeline¶
- RegBankSelect modes (
Fast: first valid mapping;Greedy: compare mappings by cost including repair copies): speed versus fewer cross-bank copies. - Localizer: moves constant materializations next to their uses before selection, so their live ranges are short and folding into immediates is possible. It appears in the AArch64 pass list of §7.
- Fallback (
-global-isel-abort=0/1/2): error, or fall back to SelectionDAG silently or with a warning. This lets a target adopt GlobalISel incrementally.
GlobalISel combiners¶
- Pre- vs post-legalizer vs post-legalizer lowering: canonicalize early, clean up legalization artifacts late, and do target-specific lowering last (AArch64 has all three).
- Rules in TableGen vs C++:
GICombineRulewith MIR patterns gives generated matchers and fewer bugs. The escape hatch is C++match/applybodies, which most rules still use for their predicates.
7. In real compilers¶
FastISel¶
FastISel::selectInstruction, FastISel::selectOperator and FastISel::tryToFoldLoad in llvm/lib/CodeGen/SelectionDAG/FastISel.cpp. The bottom-up walk with fallback is in SelectionDAGISel::SelectAllBasicBlocks (SelectionDAGISel.cpp), and the x86 hooks are in X86FastISel.cpp (X86SelectAddress, fastSelectInstruction) (LLVM 23.1.2) [LLVM-FastISel].
FastISel at -O0: a local load fold, and two fallbacks
Reproduce (llc 23.1.2, from the repository root):
cd labs/ch21-mir/inputs
for f in true false; do
echo "== fast-isel=$f"
llc -O0 -mtriple=x86_64-linux-gnu -fast-isel=$f -stop-after=finalize-isel sum.ll -o - \
| sed -n '/^ bb.1.loop/,/^ bb.2/p' | grep -vE 'successors|^\s*$|^ bb\.'
done
llc -O0 -mtriple=x86_64-linux-gnu -fast-isel-report-on-fallback fb.ll -o /dev/null
llc -O0 -mtriple=x86_64-linux-gnu -pass-remarks-missed=sdagisel fb.ll -o /dev/null
Output (complete):
== fast-isel=true
%0:gr64_nosp = PHI %9, %bb.0, %10, %bb.1
%1:gr64 = PHI %9, %bb.0, %12, %bb.1
%12:gr64 = ADD64rm %1, %5, 8, %0, 0, $noreg, implicit-def $eflags :: (load (s64) from %ir.p)
%10:gr64 = ADD64ri32 %0, 1, implicit-def $eflags
CMP64rr %10, %7, implicit-def $eflags
JCC_1 %bb.1, 12, implicit $eflags
== fast-isel=false
%0:gr64_nosp = PHI %6, %bb.0, %3, %bb.1
%1:gr64 = PHI %6, %bb.0, %2, %bb.1
%8:gr64 = MOV64rm %4, 8, %0, 0, $noreg :: (load (s64) from %ir.p)
%2:gr64 = ADD64rr %1, killed %8, implicit-def dead $eflags
%3:gr64 = INC64r %0, implicit-def dead $eflags
%9:gr64 = SUB64rr %3, %5, implicit-def $eflags
JCC_1 %bb.1, 12, implicit $eflags
JMP_1 %bb.2
warning: Instruction selection used fallback path for mul128
warning: Instruction selection used fallback path for vadd
remark: <unknown>:0:0: FastISel didn't lower all arguments: i128 (i128, i128) (in function: mul128)
remark: <unknown>:0:0: FastISel missed terminator: ret i128 %m (in function: mul128)
remark: <unknown>:0:0: FastISel didn't lower all arguments: <4 x i32> (<4 x i32>, <4 x i32>) (in function: vadd)
What to notice: FastISel's loop is the §3 trace: ADD64rm from tryToFoldLoad,
ADD64ri32 from the table, and CMP64rr/JCC_1 from the branch hook. SelectionDAG at -O0
does not fold the load: IsProfitableToFold returns false at -O0 (Lesson 21.4 §7).
SelectionDAG also emits an explicit JMP_1 to the fall-through block, which is removed later.
The last lines show the fallback of Proposition 21.6.4: i128 and vector arguments are
beyond FastISel's argument lowering, so those whole functions go to SelectionDAG.
GlobalISel pipeline¶
IRTranslator::runOnMachineFunction and IRTranslator::translate (llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp), Legalizer::legalizeMachineFunction with LegalizerHelper::legalizeInstrStep, narrowScalar and widenScalar (Legalizer.cpp, LegalizerHelper.cpp), RegBankSelect::assignInstr (RegBankSelect.cpp) and InstructionSelect::selectInstr (InstructionSelect.cpp). AArch64's parts are in llvm/lib/Target/AArch64/GISel/ (AArch64LegalizerInfo.cpp, AArch64RegisterBankInfo.cpp, AArch64InstructionSelector.cpp) (LLVM 23.1.2) [LLVM-GlobalISelSrc].
a[i] = p[3] through GlobalISel on AArch64
Reproduce (llc 23.1.2, from the repository root):
cd labs/ch21-mir/inputs
for p in irtranslator legalizer regbankselect instruction-select; do
echo "== $p"
llc -O2 -mtriple=aarch64-linux-gnu -global-isel -stop-after=$p store.ll -o - | sed -n '/^body/,$p' | sed 's/[[:space:]]*$//'
done
Output (complete):
== irtranslator
body: |
bb.1 (%ir-block.0):
liveins: $x0, $x1, $x2
%0:_(p0) = COPY $x0
%1:_(i64) = COPY $x1
%2:_(p0) = COPY $x2
%3:_(i64) = G_CONSTANT i64 24
%4:_(p0) = nuw nusw inbounds G_PTR_ADD %2, %3(i64)
%5:_(i64) = G_LOAD %4(p0) :: (load (i64) from %ir.q)
%6:_(i64) = G_CONSTANT i64 8
%7:_(i64) = nsw G_MUL %1, %6
%8:_(p0) = nusw inbounds G_PTR_ADD %0, %7(i64)
%9:_(p0) = COPY %8(p0)
G_STORE %5(i64), %9(p0) :: (store (i64) into %ir.e)
RET_ReallyLR
...
== legalizer
body: |
bb.1 (%ir-block.0):
liveins: $x0, $x1, $x2
%0:_(p0) = COPY $x0
%1:_(i64) = COPY $x1
%2:_(p0) = COPY $x2
%3:_(i64) = G_CONSTANT i64 24
%4:_(p0) = nuw nusw inbounds G_PTR_ADD %2, %3(i64)
%5:_(i64) = G_LOAD %4(p0) :: (load (i64) from %ir.q)
%10:_(i64) = G_CONSTANT i64 3
%7:_(i64) = nsw G_SHL %1, %10(i64)
%8:_(p0) = nusw inbounds G_PTR_ADD %0, %7(i64)
G_STORE %5(i64), %8(p0) :: (store (i64) into %ir.e)
RET_ReallyLR
...
== regbankselect
body: |
bb.1 (%ir-block.0):
liveins: $x0, $x1, $x2
%0:gpr(p0) = COPY $x0
%1:gpr(i64) = COPY $x1
%2:gpr(p0) = COPY $x2
%3:gpr(i64) = G_CONSTANT i64 24
%4:gpr(p0) = nuw nusw inbounds G_PTR_ADD %2, %3(i64)
%5:gpr(i64) = G_LOAD %4(p0) :: (load (i64) from %ir.q)
%10:gpr(i64) = G_CONSTANT i64 3
%7:gpr(i64) = nsw G_SHL %1, %10(i64)
%8:gpr(p0) = nusw inbounds G_PTR_ADD %0, %7(i64)
G_STORE %5(i64), %8(p0) :: (store (i64) into %ir.e)
RET_ReallyLR
...
== instruction-select
body: |
bb.1 (%ir-block.0):
liveins: $x0, $x1, $x2
%0:gpr64sp = COPY $x0
%1:gpr64 = COPY $x1
%2:gpr64sp = COPY $x2
%5:gpr64 = LDRXui %2, 3 :: (load (i64) from %ir.q)
STRXroX %5, %0, %1, 0, 1 :: (store (i64) into %ir.e)
RET_ReallyLR
...
What to notice: Definition 21.6.1's three stages of a virtual register: %5:_(i64),
%5:gpr(i64), %5:gpr64. The IRTranslator translates the GEP literally (G_MUL by the
element size). The G_MUL → G_SHL change you see "after the legalizer" was made by the
combiner that runs before it (next technique), and the legalizer had nothing to do because
i64 and p0 operations are legal. InstructionSelect folds the single-use G_PTR_ADD/G_SHL
chains into the addressing modes of LDRXui and STRXroX, the same two instructions
SelectionDAG selects (Lesson 21.5).
GlobalISel on x86-64: incomplete patterns and fallbacks
Reproduce (llc 23.1.2, from the repository root):
cd labs/ch21-mir/inputs
llc -O2 -mtriple=x86_64-linux-gnu -global-isel -stop-after=instruction-select store.ll -o - \
| sed -n '/^body/,$p' | sed 's/[[:space:]]*$//'
llc -O2 -mtriple=x86_64-linux-gnu -global-isel -global-isel-abort=2 wide.ll -o /dev/null
Output (complete):
body: |
bb.1 (%ir-block.0):
liveins: $rdi, $rdx, $rsi
%0:gr64 = COPY $rdi
%1:gr64 = COPY $rsi
%2:gr64 = COPY $rdx
%5:gr64 = MOV64rm %2, 1, $noreg, 24, $noreg :: (load (s64) from %ir.q)
%7:gr64_nosp = nsw SHL64ri %1, 3, implicit-def dead $eflags
%8:gr64 = nusw inbounds LEA64r %0, 1, %7, 0, $noreg
MOV64mr %8, 1, $noreg, 0, $noreg, %5 :: (store (s64) into %ir.e)
RET 0
...
warning: Instruction selection used fallback path for div128
warning: Instruction selection used fallback path for pop
What to notice: x86's GlobalISel selector does fold p + 24 into the load, but it does
not match the scaled-index form of the store address. It emits a shift, an LEA and a plain
store: four instructions where SelectionDAG needs two (task 4 of the MIR exploration). With
-global-isel-abort=2, functions it cannot legalize or select (__int128 division, ctpop)
fall back to SelectionDAG with a warning. This is the incremental adoption path of §6.
GlobalISel combiners¶
The combiner driver is Combiner in llvm/lib/CodeGen/GlobalISel/Combiner.cpp, the generic rules are in llvm/include/llvm/Target/GlobalISel/Combine.td (mul_to_shl, sub_to_add, trivial_combines), their C++ halves are in CombinerHelper.cpp (matchCombineMulToShl), and AArch64 instantiates them in AArch64PreLegalizerCombiner.cpp and AArch64Combine.td (LLVM 23.1.2) [LLVM-GISel-Combine].
The pre-legalizer combiner turns G_MUL by 8 into G_SHL by 3
Reproduce (llc 23.1.2; -stop-after accepts the combiner's pass name, which
-debug-pass=Structure lists):
cd labs/ch21-mir/inputs
llc -O2 -mtriple=aarch64-linux-gnu -global-isel -stop-after=aarch64-prelegalizer-combiner \
store.ll -o - | sed -n '/^body/,$p' | sed 's/[[:space:]]*$//'
curl -sL https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-23.1.2/llvm/include/llvm/Target/GlobalISel/Combine.td \
| sed -n '/^def mul_to_shl/,/>;/p'
Output (complete):
body: |
bb.1 (%ir-block.0):
liveins: $x0, $x1, $x2
%0:_(p0) = COPY $x0
%1:_(i64) = COPY $x1
%2:_(p0) = COPY $x2
%3:_(i64) = G_CONSTANT i64 24
%4:_(p0) = nuw nusw inbounds G_PTR_ADD %2, %3(i64)
%5:_(i64) = G_LOAD %4(p0) :: (load (i64) from %ir.q)
%10:_(i64) = G_CONSTANT i64 3
%7:_(i64) = nsw G_SHL %1, %10(i64)
%8:_(p0) = nusw inbounds G_PTR_ADD %0, %7(i64)
G_STORE %5(i64), %8(p0) :: (store (i64) into %ir.e)
RET_ReallyLR
...
def mul_to_shl : GICombineRule<
(defs root:$d, unsigned_matchinfo:$matchinfo),
(match (G_MUL $d, $op1, $op2):$mi,
[{ return Helper.matchCombineMulToShl(*${mi}, ${matchinfo}); }]),
(apply [{ Helper.applyCombineMulToShl(*${mi}, ${matchinfo}); }])>;
What to notice: the rule is Definition 21.6.7: a MIR match pattern rooted at G_MUL,
a C++ predicate that fills matchinfo (the shift amount), and an apply. In the output,
%6 = G_CONSTANT 8 is gone (erased as dead), %10 = G_CONSTANT 3 is new, and %7 keeps its
number because the instruction was rewritten in place. The redundant %9 = COPY %8 was also
removed by another trivial combine (copy propagation).
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| FastISel | one IR instruction at a time plus local folds; falls back for the rest (Proposition 21.6.4) | linear, smallest constant | -O0 quality; debuggable 1:1 mapping |
moderate per target (hooks) | -O0 on x86-64 and other SelectionDAG targets |
| GlobalISel pipeline | whole-function generic MIR; legal → banked → selected (Theorem 21.6.6) | linear passes, no per-block DAG | on AArch64 comparable to SelectionDAG; on x86-64 still incomplete (4 instructions vs 2 on the store box; fallbacks) | high: four target components, but reusable and testable in MIR | AArch64 (-O0 default), AMDGPU, RISC-V, Apple GPU; experimental x86 |
| GlobalISel combiners | canonicalization and simplification on MIR, across blocks | linear per iteration, capped | quality depends on the rule set | rules in TableGen plus C++ helpers | pre-/post-legalizer passes of every GlobalISel target |
- Choose FastISel when you need
-O0speed on a SelectionDAG target and good debug info. It stays as long as SelectionDAG is the target's main selector. - Choose GlobalISel when you write a new target, need cross-block selection or register-bank decisions, or want each stage testable on MIR (
llc -run-pass=legalizeron a.mirfile).
9. Assessment¶
| Technique | Quiz ids (solutions/quizzes/ch21.yaml) |
Drill | Flashcard tag | Exercises |
|---|---|---|---|---|
| FastISel | fastisel-fallback, fastisel-o0-fold |
none: FastISel is a table plus hooks; the MIR exploration exercises it | fastisel |
E5 task 8 |
| GlobalISel pipeline | gisel-pass-order, gisel-bank |
./course drill legalization (actions), plus E5 |
globalisel |
E5 tasks 3–4 |
| GlobalISel combiners | gisel-mul-to-shl, gisel-combine-where |
none: rule-specific, quizzed on the mul_to_shl example |
gisel-combiner |
E5 task 3 |
Reading -stop-after=legalizer as the legalizer's work
The pipeline runs several passes before the Legalizer (IRTranslator, the pre-legalizer
combiner, the Localizer, a load/store optimizer). A change visible after -stop-after=legalizer
may come from any of them, as G_MUL → G_SHL does here. To attribute a change to one pass,
stop before and after it (-stop-before=legalizer, -stop-after=legalizer) or run the
pass alone on a .mir file with llc -run-pass=legalizer.
References¶
See the chapter references.