LLVM has several cost-modeling used by different middle-end and back-end passes. The most commonly referenced ones are:
- Target Transform Info (TTI) cost model (IR-level “how expensive is this op on this target?”)
- Loop Vectorizer cost model (profitability and vector width selection)
- SLP Vectorizer cost model (profitability of packing scalar ops into vectors)
- Inline cost model (inliner profitability)
- MachineScheduler / CodeGen scheduling models (back-end instruction scheduling / itineraries)
- Register allocation / spill cost heuristics (cost of keeping values in registers vs spilling)
- Instruction selection / pattern selection “cost” heuristics (choose cheaper machine patterns)
- Analysis-based (BPI/BFI) profile-driven “cost” (not a direct op-cost model, but used to weigh “hot” vs “cold” decisions)
TargetTransformInfo (TTI) is LLVM’s target-aware query interface used by IR (middle-end) optimizations to answer questions like:
- “How expensive is this IR instruction on this target CPU?”
- “Is this vector type legal and efficient?”
- “Will this operation lower to a single machine instruction or a long expansion?”
- “How costly are shuffles, reductions, gathers, masked ops, and address computations?”
TTI exists because LLVM’s middle-end optimizations (vectorization, unrolling, inlining heuristics, etc.) should be target-aware without embedding CPU-specific logic inside every pass.
- Not a cycle-accurate simulator.
- Not guaranteed to map 1:1 to real latency/throughput.
- Not universal across passes as “absolute truth”.
TTI’s primary purpose is relative comparison:
If TTI says option A costs less than option B, LLVM assumes A is more profitable on that target.
At a high level:
- IR optimization pass (e.g., LoopVectorize, SLPVectorizer, unrolling heuristics) needs profitability estimates.
- It queries TTI for cost/legality.
- TTI is implemented by each target backend (e.g., X86, AArch64, RISC-V), usually using:
- subtarget feature flags (AVX2, AVX-512, NEON, SVE…)
- type legality information
- lowering rules (e.g., “this becomes a libcall”)
- instruction selection patterns and known costs
TTI forms the “contract” between target-independent IR passes and target-specific knowledge.
Many transformations improve “instruction count” but might hurt real performance due to:
- expensive instructions (division, remainder, transcendental math)
- expensive vector shuffles/permutations
- scalarization when vector types are unsupported
- heavy runtime checks (alias checks, alignment checks)
- memory system realities (loads/stores vs ALU ops)
- target constraints (register pressure, ABI calls, etc.)
TTI helps guide decisions before lowering to machine code.
TTI usually returns costs in an abstract unit (often “instruction-like” units).
Important properties:
- Costs are designed to be comparable within one target and context.
- Cost of 10 isn’t necessarily “10 cycles”.
- Some targets try to approximate throughput/latency, but it’s heuristic.
In practice, passes do comparisons like:
Cost(vectorized_loop) + overhead < Cost(scalar_loop)Cost(pack+vector_op+unpack) < Cost(scalar_ops)
Examples:
- arithmetic:
add,mul,fadd,fmul,fdiv - comparisons:
icmp,fcmp - selection:
select - casts/conversions:
zext,sext,trunc,bitcast,fptosi,sitofp - vector ops:
shufflevector,insertelement,extractelement - calls:
call(incl. intrinsics) and potential libcall lowering
Examples:
- cost of load/store of type
i32,i64,<4 x float>, etc. - sensitivity to alignment
- whether a vector memory operation will become:
- a single vector load/store (cheap)
- multiple scalar loads/stores (expensive)
- gather/scatter sequences (often expensive)
TTI can answer:
- Is
<8 x i32>legal? - Is
<16 x i8>legal? - Will it widen/narrow, split, or scalarize?
TTI often participates in cost estimation for:
- reductions (horizontal add/min/max)
- gather/scatter
- masked loads/stores
- interleaved memory ops
- address computations (folding into addressing modes)
A simplified profitability framework:
- Compute scalar cost:
- sum of per-instruction costs
- Compute transformed cost:
- sum of per-instruction costs in new form
- Add overheads:
- shuffles, setup, runtime checks, remainder handling
- Decide:
- transform if
NewCost + Overhead < OldCost
- transform if
TTI supplies the per-instruction and per-pattern costs.
- Scalar
%a = add i32 %x, %y
%b = add i32 %p, %q
%c = add i32 %m, %n
%d = add i32 %u, %v- Potential vector form
%vx = <4 x i32> ...
%vy = <4 x i32> ...
%vr = add <4 x i32> %vx, %vyTTI considerations:
- If
<4 x i32>matches the hardware SIMD width (e.g., 128-bit or 256-bit vectors), TTI returns a low cost for the vector add. - If the type is not supported, LLVM may “legalize” it by splitting/scalarizing, e.g.:
- split
<8 x i32>into two<4 x i32>ops - or scalarize into 8 scalar adds
- split
Decision:
- If vector add cost + packing/unpacking is cheaper than 4 scalar adds → accept.
- Otherwise reject.
- IR division
%q = sdiv i32 %a, %bDivision is typically expensive compared to add/mul.
- Power-of-two divisor
If
%b = 8, a transform may replace:
%q = ashr i32 %a, 3TTI reasoning:
Cost(sdiv)is usually high.Cost(ashr)is low.- Therefore, strength reduction is profitable.
- Constant non-power-of-two divisor LLVM can transform divide-by-constant into a “magic” multiply+shift sequence:
- Several integer ops but often still cheaper than hardware division on many CPUs.
TTI helps decide if:
- hardware division cost > sequence cost
Suppose SLP tries to pack scalars:
%v0 = ...
%v1 = ...
%v2 = ...
%v3 = ...Packing into a vector may require:
insertelementchainshufflevectorpermutationsextractelementlater
Even if the vector ALU op is cheap, shuffles may be expensive.
TTI is critical here because shuffle/permutation cost varies drastically by target:
- Some ISAs have cheap lane permutations.
- Others require multiple instructions or have throughput bottlenecks.
Profitability check (conceptual):
- Scalar:
4 * Cost(add) - Vector:
Cost(pack) + Cost(vector add) + Cost(unpack) - Transform only if Vector < Scalar.
- Scalar loop
for (i=0; i<n; i++)
if (a[i] > 0) b[i] = a[i];Vectorization introduces masks:
- compare -> mask
- masked store (or blend/select + store)
TTI questions:
- Does the target have efficient masked load/store?
- Does it lower to a single instruction, or expand into scalar control flow?
- How expensive is
selectand mask handling?
Outcomes:
- On a target with strong predication/masked ops → vectorization likely profitable.
- If masked ops expand heavily → vectorization may be rejected.
- Indexed access
b[i] = a[idx[i]];This is not contiguous, so vectorizing may require gather.
TTI considerations:
- Is vector gather supported?
- If supported, is it expensive (often yes)?
- If not supported, will it scalarize to:
- multiple scalar loads
- inserts into a vector
- possible extra control flow
Often: Cost(gather) is high → vectorization not profitable unless loop is very hot or there are additional wins.
IR intrinsic:
%r = call double @llvm.sqrt.f64(double %x)TTI may model:
- If lowered to hardware
sqrtinstruction: moderate cost. - If lowered to a libcall
sqrt():- call overhead + ABI constraints + potential pipeline disruption → high cost.
This influences:
- vectorization (vector sqrt availability)
- hoisting (move out of loop if expensive)
- approximation decisions (if allowed by flags)
Many targets have rich addressing modes. Example conceptually:
- base + index*scale + offset folded into a load/store
IR might compute an address in steps:
%idx = mul i64 %i, 4
%ptr = getelementptr i8, ptr %base, i64 %idx
%val = load i32, ptr %ptrTTI can reflect that some address arithmetic is “effectively free” if it can be folded into the load/store addressing mode.
This affects transformations like:
- loop strength reduction
- combining GEPs
- choosing induction variable forms
- Loop Vectorizer Uses TTI (and its own overhead accounting) to decide:
- vectorization factor (VF)
- interleave factor
- whether masked operations are worth it
- whether to scalarize certain instructions within vector loop (partial vectorization)
- SLP Vectorizer Uses TTI to estimate:
- tree cost (packed operations)
- shuffle/permute overhead
- whether it is better to keep scalar
Rule of thumb: vectorizers are only as good as:
- legality info + shuffle cost + memory cost modeling which are exactly where TTI provides target-specific knowledge.
TTI costs vary based on:
- enabled ISA features (e.g., SSE4.2 vs AVX2 vs AVX-512)
- native vector register width and preferred types
- lowering rules (some IR ops become sequences)
- costs of shuffles/permutes
- memory alignment penalties
- fast-math / strict FP mode constraints
So the same IR can vectorize on one target and not on another.
When you see LLVM choose not to vectorize/unroll or not to form vectors, likely reasons (often reflected through TTI) include:
- Vector type illegal → scalarization cost too high.
- Shuffle tax → too many permutes to pack/unpack.
- Masked/gather operations too expensive.
- Expensive instruction dominates and doesn’t vectorize well.
- Runtime checks overhead outweighs benefit.
Compared to a “generic” target, X86 has:
- Many ISA feature levels (SSE2 → SSE4.2 → AVX → AVX2 → AVX-512 + subfeatures)
- Rich addressing modes (base + index*scale + displacement) that can make some address arithmetic “free”
- Expensive or tricky shuffles/permutations depending on width and instruction class
- Clear performance cliffs around:
- integer division/remainder
- gathers (especially pre-AVX-512)
- unaligned/misaligned access (usually okay, but can still be costly in some cases)
- cross-lane shuffles (256-bit AVX2 split into 128-bit lanes for many operations)
- ABI/library-call lowering for some operations (especially strict FP, long double, certain math)
X86 TTI attempts to capture these at IR level so that mid-end passes can choose better transforms.
TTI costs for the same IR can change drastically with:
+sse2,+sse4.1,+avx,+avx2,+avx512f,+avx512bw,+avx512dq,+avx512vl, etc.- CPU model (e.g.,
-mcpu=skylake,znver3, etc.) - whether vector widths are preferred/penalized (e.g., 256-bit vs 512-bit decisions)
- availability of specific ops:
- FMA (
vfmadd*) - vector integer multiply (wider sizes)
- masked operations (AVX-512 masks are a big deal)
- vector gather/scatter (AVX2 gather exists; AVX-512 improves masking and throughput patterns)
- FMA (
Practical implication:
Always interpret “TTI cost” as “TTI cost under the active subtarget features.”
A loop might vectorize under-mattr=+avx2but not under+sse2, and TTI is one reason.
- SSE: 128-bit XMM registers
- AVX/AVX2: 256-bit YMM registers
- AVX-512: 512-bit ZMM registers
TTI tends to view operations as cheaper when the vector width and element types map neatly to native instructions.
IR:
%r = fadd <8 x float> %a, %b- On SSE-only:
<8 x float>typically splits into two<4 x float>ops → higher cost. - On AVX:
<8 x float>maps to one 256-bitvaddps→ lower cost. - On AVX-512: could use 512-bit for
<16 x float>as one op, but:- some CPUs may have different frequency/throughput behavior for 512-bit ops
- cost model may penalize 512-bit usage depending on CPU tuning
Vectorizer uses TTI to choose vectorization factor and possibly avoid overly wide vectors when not beneficial.
On X86, shuffle cost depends on:
- whether it’s within 128-bit lanes (cheaper on AVX2)
- cross-lane permutations (often more expensive)
- whether it can be done by a single
pshufd,pshufb,vperm2f128,vpermd, etc. - whether it requires multiple instructions
Many AVX2 operations are effectively two 128-bit lanes glued together. Some permutations across lanes require extra instructions.
SLP vectorizer and Loop vectorizer rely on TTI’s shuffle cost to decide if:
- packing scalars is profitable
- rearranging data layout is worth it
- vectorizing with a given VF causes too many permutes
Rule of thumb on X86:
If a transformation introduces many
shufflevectoroperations, the X86 TTI cost often rises quickly, and LLVM may refuse vectorization.
X86 has strong addressing modes:
[base + index*scale + disp] where scale is 1,2,4,8.
%idx = shl i64 %i, 2 ; i*4
%ptr = getelementptr i8, ptr %base, i64 %idx
%val = load i32, ptr %ptrOn X86, the i*4 can often be folded into the addressing mode of the load:
- the shift/mul may not need a separate instruction in the final assembly
TTI may model this by not charging as much (or sometimes not charging at all) for address arithmetic if it’s likely foldable.
Why it matters:
- loop strength reduction decisions
- induction variable forms
- GEP reassociation and address computation hoisting/sinking
IR:
%q = sdiv i32 %a, %b
%r = srem i32 %a, %bThese are typically high cost on X86:
idivis slow and has low throughput
TTI will usually mark them expensive, encouraging:
- strength reduction (shift/magic multiply when divisor is constant)
- hoisting out of loops when possible
- vectorization avoidance if division dominates and doesn’t vectorize well
fdiv and sqrt may be more expensive than fmul/fadd.
Depending on fast-math flags and available instructions, the cost can differ.
Indexed load pattern:
b[i] = a[idx[i]];Vectorizer may consider using gather:
- On AVX2:
vgather*exists but often high latency/low throughput; also mask handling differs. - On AVX-512: gathers integrate better with masking, and the model may treat them differently (but still not “cheap”).
TTI commonly makes gathers a high-cost operation, so LLVM tends to avoid vectorizing such loops unless there’s enough other benefit.
Predication often requires:
- vector compares + blends/selects
- sometimes extra moves/shuffles
- masked store may be emulated
Many vector operations support mask registers (k regs):
- masked loads/stores
- masked arithmetic
TTI often reflects that:
- masked vectorization becomes more attractive under AVX-512
- conditional loops are more likely to vectorize profitably
Example loop:
if (a[i] > 0) b[i] += a[i];Under AVX-512, the cost of mask handling is often lower than on AVX2/SSE, improving vectorization profitability.
Reduction:
sum += a[i];Vectorization produces:
- vector partial sums
- final horizontal add (“reduce”)
On X86, reduction cost depends on:
- available horizontal add instructions (
haddps) vs shuffle+add sequences - vector width (128/256/512)
- whether the reduction can be unrolled to hide latency
TTI helps decide:
- VF choice (e.g., 8 floats with AVX vs 4 floats with SSE)
- whether reduction overhead outweighs benefit for small trip counts
For something like:
a[i] = b[i] + c[i];X86 TTI usually says:
- vector loads/stores +
vaddpsare cheap - vectorization is profitable
If your loop requires frequent permutations (AoS ↔ SoA style conversions), X86 TTI may increase cost enough that LLVM refuses.
Even if AVX2 has vgather, TTI often discourages it unless necessary.
- AVX-512 can make masking and some vector patterns cheaper
- but may also have tuning concerns regarding very wide vectors on some CPUs So the “best VF” may change.
for (i=0; i<n; i++)
y[i] = x[i] * 3.0f + 1.0f;X86 TTI usually favors vectorization because:
- contiguous loads/stores
- simple arithmetic maps to vector instructions
- FMA may reduce cost further if available
for (i=0; i<n; i++)
if (x[i] > 0) y[i] += x[i];On AVX2, mask handling uses blends and may be expensive. On AVX-512, masked ops are more direct → more likely profitable.
for (i=0; i<n; i++)
y[i] = x[idx[i]];TTI often makes gather/scalarization expensive → vectorization often rejected.
The Loop Vectorizer has its own cost model (built on top of TTI) to decide:
- whether to vectorize a loop
- what vectorization factor (VF) to choose (e.g., 4, 8, 16)
- whether to use runtime checks (alignment, alias checks)
- whether to use interleaving/unrolling alongside vectorization
- whether scalar epilogs / remainder handling is worth it
It estimates cost of:
- scalar loop body
- vectorized body for each candidate VF
- overhead costs:
- vector loop setup
- mask handling
- reductions
- gathers/scatters (if non-contiguous access)
- runtime checks
C-like:
for (i=0; i<n; i++)
a[i] = b[i] + c[i];Vectorizer considers:
- contiguous loads/stores → usually good
- expected speedup roughly ~VF (bounded by memory bandwidth)
It might choose VF=8 on AVX2 (256-bit vectors) for i32 or float, if legal.
for (i=0; i<n; i++)
if (x[i] > 0) y[i] += x[i];Vectorization may require:
- vector compare to produce mask
- masked load/store or blend/select
- extra overhead
Cost model decides if masked vector loop beats scalar.
sum = 0;
for (i=0; i<n; i++) sum += a[i];Vectorization introduces:
- vector partial sums
- horizontal reduction at end
Cost model includes reduction overhead.
Loop Vectorizer is a specialized cost model for loops that uses TTI instruction costs plus loop-specific overhead modeling.
The SLP Vectorizer vectorizes within a basic block by:
- finding isomorphic scalar instruction sequences (trees)
- packing them into vector instructions
- inserting shuffles/extracts as needed
Its cost model compares:
- scalar cost of N independent operations
- vector cost (packed op + required shuffles)
- decides if packing is profitable
Scalar:
%a0 = add i32 %x0, %y0
%a1 = add i32 %x1, %y1
%a2 = add i32 %x2, %y2
%a3 = add i32 %x3, %y3SLP might transform into:
- build vectors
<4 x i32>from scalars (inserts) - one vector
add - extract results if needed
If inserts/extracts/shuffles are too expensive (or if results remain scalar), cost model may reject.
If operands are not nicely aligned in registers or memory, SLP may need:
shufflevectoroperations- extra permutes (costly on some ISAs)
Cost model includes shuffle/permutation cost via TTI.
SLP profitability depends heavily on shuffle cost and whether results stay in vector form.
LLVM’s inliner uses an inline cost computed from:
- estimated size/complexity of callee
- callsite properties (attributes, constant args, etc.)
- bonuses for simplifying (constant propagation, removing branches)
- penalties for code growth
- thresholds (default or profile-guided)
This is not an “instruction latency” model; it’s a code-size / simplification heuristic.
- Functions marked
alwaysinlinebypass cost model (unless impossible). - Otherwise, something like:
- tiny leaf function → inline
- large function with loops → likely not inline unless hot callsite
int f(int x, int flag) {
if (flag) return x+1;
else return x-1;
}At callsite f(a, 1), inlining allows constant-folding away the branch, giving a “bonus” in inline cost.
Inliner cost model trades code growth vs optimization opportunity.
In the back-end, LLVM uses scheduling models (per target) to estimate:
- instruction latency
- reciprocal throughput
- resource usage (ports/pipelines)
- micro-op decomposition (on some targets)
This guides:
- instruction scheduling (MachineScheduler / post-RA scheduler)
- sometimes impacts unrolling decisions at the machine level
If a target’s model says:
imullatency 3 cyclesidivlatency 20+ cycles Scheduler tries to:- move independent instructions to cover latency
- avoid resource conflicts
This cost model is more “microarchitectural” than TTI and is used after lowering in machine instructions.
Register allocation uses heuristics like:
- spill weight / spill cost (how expensive to spill a value to stack and reload)
- live range splitting decisions
- rematerialization (recompute a value instead of spilling)
If a value is:
%v = add i32 %a, 1It might be cheaper to recompute add later than spill/reload, depending on:
- usage frequency
- loop nesting depth (hotness)
- instruction cost vs memory op cost
This is a “cost model” in the sense of estimating profitability for allocation decisions.
costs come from a mix of:
- legality and type-lowering decisions,
- pattern/predicate priority and complexity,
- per-target heuristics about “cheap” instructions and folds,
- and (sometimes) explicit or implicit “cost” comparisons.
In ISel, cost modelling is mostly about choosing among multiple legal lowerings.
Typical dimensions:
- Instruction count / pattern complexity
- Microarchitectural cost hints (latency/throughput/resource usage; more prominent post-ISel in scheduling)
- Code size (sometimes prefer shorter encodings)
- Folding opportunities (combine ops into one instruction)
- Register pressure / constraints (special regs, partial regs, flag dependencies)
- Addressing modes (can we fold scales/offsets)
- Vector width and shuffle complexity (permute costs differ a lot)
prefer patterns that produce fewer / simpler instructions and enable folding.
Before comparing two patterns, LLVM must ensure operations are legal on the target:
- If an operation/type is illegal, LLVM must expand or legalize it (split, widen, scalarize, libcall).
- This expansion is effectively a huge cost penalty.
Example (generic): illegal i128 multiply LLVM IR:
%p = mul i128 %a, %bIf the target has no native i128 multiply:
- ISel/legalizer expands into multiple i64 multiplies and adds (long sequence).
- Even without explicit “cost numbers,” legality drives the outcome:
- “native single instruction” (if exists) is chosen
- otherwise “expanded sequence” is the only legal path
Takeaway: In ISel, “legal vs illegal” often dominates cost decisions.
In SelectionDAG ISel, TableGen patterns are matched. Patterns have:
- predicates (
SubtargetFeaturechecks) - complexity ordering and specificity
- sometimes explicit “AddedComplexity”
In GlobalISel, the selector similarly chooses from legal patterns/instruction mappings and prefers more constrained matches.
Example (generic): fused multiply-add IR:
%r = fadd float (fmul float %a, %b), %cIf target has FMA:
- prefer one
fmainstruction Else: - select
multhenadd
This is “cost modelling” via:
- fewer instructions,
- better precision semantics (if allowed),
- and a pattern that only exists when features allow.
Cost modelling in ISel is also influenced by:
- IR combines before ISel (InstCombine, DAGCombine, MachineCombiner)
- target-specific DAG combines (e.g., fold constants, address patterns)
- post-isel peepholes
These combines often apply rules like:
- replace expensive op with cheaper sequence
- fold ops into addressing modes
- choose shorter or faster pattern
A classic “cost” decision (sometimes earlier than ISel, but strongly target-shaped):
IR:
%r = select i1 %cond, i32 %x, i32 %yPossible lowerings:
- conditional move (
cmov) style instruction - branch + phi (control flow)
Generic heuristics consider:
- target support for cmov/predication
- predicted branch probability (if known)
- code size and pipeline effects
Often, the final mapping is done in or near ISel because it’s target-dependent.
X86 is an excellent case study because it offers:
- rich addressing modes
- many vector shuffle instructions
- special flag dependencies (
EFLAGS) - multiple instruction forms for the same operation (reg-reg, reg-mem, mem-reg)
LEAas a “free-ish” arithmetic instruction in many contextsCMOVandSETccpatterns- AVX/AVX2 lane behavior and AVX-512 masking
choosing the best instruction form and maximizing folding.
X86 can encode memory operands like:
[base + index*scale + disp]
IR-like intent:
x = *(int*)(base + i*4 + 16);Naive instruction sequence:
imulto scaleiaddbaseadddispload
X86 ISel often produces:
- a single
mov(load) using a complex addressing mode:mov eax, dword ptr [rdi + rsi*4 + 16]
Cost model behavior:
- Folding address computation into the memory op reduces instruction count and register usage.
- X86 ISel strongly prefers patterns that enable such folds.
If IR needs:
t = a + b*4 + 8;X86 may use:
lea rax, [rdi + rsi*4 + 8]
Why LEA is “cheap” in X86 modelling:
- Often single instruction
- Doesn’t clobber flags (unlike
add/sub) - Great for address-like arithmetic
So ISel frequently prefers LEA-based patterns for add+mul-by-constant combinations.
Reg-reg vs reg-mem forms: fewer instructions vs hidden costs
X86 allows many ops with a memory operand:
add eax, [mem](load + add in one instruction)
IR:
%v = load i32, ptr %p
%r = add i32 %v, %xPossible machine forms:
- separate load then add:
mov eax, [p]add eax, x
- folded memory operand:
add eax, [p]
Cost modelling tradeoff:
- Folding reduces instruction count and register pressure.
- But it also can:
- create memory dependencies in ALU ops,
- reduce scheduling flexibility,
- sometimes be slower on specific microarchitectures.
LLVM’s X86 backend generally still prefers folds when profitable, but may avoid them in some scenarios (e.g., when it blocks other combines or affects codegen patterns).
Many X86 integer ops set flags implicitly. Flags are a limited resource:
- using them can create false dependencies
- can restrict scheduling and register allocation
IR:
%cmp = icmp slt i32 %a, %b
br i1 %cmp, label %T, label %FX86 lowering:
- likely uses
cmp(sets flags) thenjl(consumes flags)
Now consider if an arithmetic op that sets flags is between cmp and branch:
- it may clobber flags → extra compare needed
Cost modelling impact:
- ISel tries to choose instruction sequences that:
- avoid unnecessary flag clobbers
- or fuse compare+branch patterns efficiently
- Sometimes prefers
leaoveraddto avoid flags clobbering if flags are needed soon.
IR:
%r = select i1 %c, i32 %x, i32 %yOn X86 with CMOV:
cmovcan implement select without a branch.
However, cmov is not always best:
- if
%cis highly predictable, a branch might be cheaper cmovcan force both%xand%ycomputations to happen eagerly
ISel cost modelling:
- often uses heuristics:
- size/simplicity
- whether operands are cheap
- probability info if available
- final choice can be influenced by later passes too
IR:
%r = mul i32 %x, 5Possible lowerings on X86:
imul reg, reg, 5lea reg, [x + x*4](x + 4x = 5x)shl+addsequence
X86 modelling commonly prefers:
leafor small constant multipliers where it fits the addressing formimulfor general constants or when LEA pattern not possible/beneficial
Cost factors:
- instruction count
- latency/throughput (varies by uarch)
- flags clobbering (LEA does not clobber flags)
IR vector shuffle:
%r = shufflevector <8 x i32> %a, <8 x i32> %b, <8 x i32> <...mask...>On X86, there are many shuffle-like instructions:
pshufd,pshufb,unpck*,palignr,blend*- AVX/AVX2:
vperm2f128,vpermd,vpshufb(lane restrictions) - AVX-512:
vpermt2*,vpermps, plus masking
Cost modelling in ISel:
- choose the instruction (or sequence) that implements the shuffle mask cheapest
- avoid cross-lane permutations when possible (AVX2 lane split)
- prefer instructions available under the active subtarget features
Practical outcome:
- the same IR shuffle mask can lower to:
- one instruction under AVX-512
- multiple instructions under AVX2
- even more under SSE2
That “difference” is a form of cost modelling driven by feature availability and pattern complexity.
TTI is mainly used by IR passes before ISel. But the relationship is important:
- IR transformations guided by TTI (e.g., vectorization, unrolling) create IR patterns.
- ISel then chooses the best X86 instructions for those patterns.
So you can think of a two-level system:
- TTI cost model (IR-level) predicts:
- “if we create this vector shuffle / gather / masked op, will it be expensive on X86?”
- ISel cost heuristics (lowering-level) decide:
- “given that we have this shuffle, which exact X86 instruction(s) is cheapest under SSE/AVX/AVX-512?”
- If a transform introduces a complex
shufflevector,- TTI might have been overly optimistic or forced by legality.
- ISel then picks among X86 shuffle instructions (or sequences), but it cannot avoid the fundamental cost.
Takeaway: Good TTI modelling reduces the chance that ISel ends up generating shuffle-heavy slow code.
int t = base[i*4 + 4];- compute index scale
- gep
- load
- fold all into one memory operand:
mov eax, [rdi + rsi*16 + 16](example form)
Cost logic:
- fewer instructions
- fewer temporary registers
- better code size
%r = select i1 %c, i32 %x, i32 %ycmovsequence (branchless)- branch + phi (control flow)
Cost logic:
cmov: good when branch unpredictable / operands already computed- branch: good when condition predictable and one side cheap to skip
%r = mul i64 %x, 3lea rax, [rdi + rdi*2](x + 2x)imul rax, rdi, 3
Cost logic:
- LEA often preferred:
- single instruction
- no flags
- good throughput on many CPUs
A complex permute on <16 x i32>
- Under AVX2:
- may require multiple instructions and lane-crossing ops
- Under AVX-512:
- might be a single permute with masks
Cost logic:
- ISel picks the cheapest legal instruction(s) available.
- Earlier, TTI would also have priced that permute differently and could influence whether the transform happens at all.
- notion of cost × hotness rather than cost alone. Even if an optimization is “profitable” in isolation, LLVM may avoid it in cold code to reduce code size, compile time, or instruction cache pressure; conversely, it may apply more aggressive transforms in hot regions.
Hotness-weighted cost means:
Apply these models more aggressively (or accept higher code growth) in hot regions, and be conservative in cold ones.
This concept shows up in:
- Inlining thresholds (hot callsite → inline more)
- Loop unroll/vectorize thresholds (hot loop → do more)
- Code layout and function ordering (hot blocks together)
- If-conversion decisions
- Machine block placement (fallthrough and branch layout)
- Partial inlining and outlining decisions
- Simplifying CFG and speculation decisions
BPI estimates the probability that a branch goes to each successor.
Sources:
- static heuristics (loop backedges likely taken, etc.)
- metadata / profile data (from PGO)
Example:
- Backedge in a loop might be predicted ~90–99% taken (heuristic)
- With PGO it can be 99.99% for big loops or 50% for small loops, etc.
BFI estimates the relative execution frequency of each basic block in a function.
Key:
- BFI is derived from BPI + CFG structure (and optionally actual counts from PGO)
- BFI gives relative weights like “block A runs 100x more than block B”
LLVM often labels blocks or callsites as:
- hot: very frequently executed
- cold: rarely executed
- warm: in-between (sometimes used)
This classification influences thresholds and heuristics.
LLVM can still estimate BPI/BFI based on:
- loop structure (backedges likely)
- branch prediction heuristics (e.g., error paths are cold, null checks often not taken, etc.)
- attributes like
cold,unlikely,__builtin_expect
This is useful, but not as accurate as real profiles.
Flow:
- Compile with instrumentation (
-fprofile-generate/-fprofile-instr-generate) - Run representative workloads → generates
.profraw - Merge to
.profdata - Recompile with
-fprofile-use/-fprofile-instr-use
What it gives LLVM:
- real edge counts (branch taken counts)
- real block execution counts
- callsite hotness and call counts
- value profile info for some optimizations (indirect calls, mem ops, etc.)
Flow:
- Build binary
- Run under a sampling profiler (e.g.,
perf) → collect samples - Convert to profile format (e.g.,
llvm-profdatafor samples) - Compile with
-fprofile-sample-use
What it gives LLVM:
- sampled instruction/address hotness
- approximate call graph hotness
- tends to be less precise for edge counts than instrumentation, but can be very effective and cheaper to deploy
LLVM does not literally do “cost × frequency” everywhere, but conceptually many decisions behave like:
- Optimize if:
SpeedupBenefit(block) * Hotness(block) > CodeGrowthPenalty
- Or increase threshold if block/callsite is hot:
InlineThreshold = Base + Bonus(hotness)
- Or avoid changes in cold blocks even if locally profitable:
- “don’t vectorize cold loops”
- “don’t unroll cold loops”
- “don’t speculate expensive operations in cold paths”
C-like:
int parse(...) {
if (unlikely(err)) return -1; // error path
// hot parsing loop
...
}CFG:
- entry → if(err) → error_return
- entry → else → hot_path
With BPI/BFI:
error_returnblock frequency is very lowhot_pathis high
Effects:
- keep error handling code small (avoid unrolling, avoid heavy inlining into it)
- prioritize optimizing the hot path
for (i=0; i<n; i++) body();Even without PGO:
- backedge likely taken many times
- blocks inside loop are “hotter” than exit blocks
With PGO:
- LLVM may learn actual average
nand branch behavior, refining BFI and thus the aggressiveness of unrolling/vectorization.
Inlining uses an inline cost model (size vs benefit). Hotness changes thresholds.
Example:
int hot_loop(...) {
for (...) total += f(x); // callsite #1, very hot
}
int cold_path(...) {
if (err) return f(x); // callsite #2, cold
}With PGO:
- callsite #1 gets a hotness bonus → inline
feven if it’s moderately large - callsite #2 might not inline to avoid bloating cold code
With profile:
- place frequently executed blocks adjacent to reduce taken branches and I-cache misses
- outline cold blocks (or place them out-of-line)
- reorder functions so hot functions are close in memory
This tends to improve instruction cache locality and branch prediction behavior.
Transform:
- convert branchy code to branchless
select(or CMOV at machine level)
Hotness decides:
- If branch is unpredictable in a hot region, branchless may win.
- If branch is predictable or cold, keep branch to reduce unnecessary work.
In X86 hotness weighting is very impactful:
On X86, especially modern OoO cores:
- mispredicted branches are expensive (pipeline flush)
- taken branches can also have front-end cost
- but predictable branches are often very cheap
Hotness-weighted implication:
- In hot code, LLVM is more willing to restructure control flow (layout, if-conversion) to reduce mispredicts.
- In cold code, it may prefer smaller code size even if branchless would be marginally faster.
Inlining increases code size, which can hurt I-cache / uop cache behavior.
With PGO:
- inline aggressively into hot loops/functions
- avoid inlining into cold paths to preserve cache locality
X86-specific note:
- Code layout and i-cache behavior are often first-order effects for performance on X86 servers and desktops.
Vectorization/unrolling can:
- increase code size
- increase register pressure
- increase decode bandwidth pressure (more instructions, more bytes)
Hotness tells LLVM where it’s worth paying these costs.
On X86:
- The benefit of vectorization can be large (SSE/AVX/AVX2/AVX-512)
- But code size and front-end throughput can limit real speedups
Thus, profile-guided hotness helps select:
- which loops to vectorize
- how much to unroll
- whether to version loops with runtime checks
At machine level, X86 has cmovcc.
Hotness-weighted guidance:
- If the condition is unpredictable and code is hot → CMOV/branchless may help
- If condition is highly biased → branch is good
- If code is cold → keep simplest/smallest
PGO provides the bias and frequency information needed.
int foo(int *a, int n, int flag) {
int sum = 0;
for (int i=0; i<n; i++) {
if (flag) sum += a[i]; // path A
else sum -= a[i]; // path B
}
return sum;
}LLVM may guess flag is unpredictable:
- consider if-conversion / select-like forms
- might create branchless form inside loop to avoid mispredicts
Suppose real workload: flag is almost always 1.
- Branch becomes highly predictable
- In hot loop, a predictable branch can outperform a branchless select (which does extra work)
- LLVM may keep the branch, or even specialize (if it can) through cloning/versioning
On X86, this can be significant because:
- predictable branches are cheap
- unnecessary arithmetic in hot loops costs throughput
This is “hotness-weighted cost” in action:
- profile says both the loop is hot and the branch is biased
- so the model favors the predictable branch.
static inline int g(int x) { return x * 3 + 1; } // small
int f(int *p, int n) {
int t = 0;
for (int i=0;i<n;i++) t += g(p[i]); // hot
if (n == 0) return g(7); // cold-ish
return t;
}With PGO:
- hot loop callsite: inline
g(benefit high, overhead removed in hot code) - cold callsite: even if it inlines, it’s less important; LLVM may still inline because it’s tiny, but for larger
g, it would avoid cold growth.
X86 angle:
- hot loop benefits from better scheduling and vectorization opportunities when
gis inlined - cold site doesn’t justify code growth (i-cache efficiency)