Skip to content

Chapter 20 · Interprocedural & Whole-Program Optimization

Part 4 · Optimization · about 3 weeks · Previous: Ch 19 · Next: Ch 21

The problem

Chapters 13–19 optimize one function at a time and treat every call as a black box: its arguments escape, its result is unknown, it may read and write anything. The input of this chapter is a whole module — or, with link-time optimization, a whole program — of LLVM IR (and, for virtual calls, the small class-based language of the comparison lab); the output is twofold. First, interprocedural facts: which functions a call may reach (the call graph), in which order to visit them (SCCs, bottom-up and top-down), what each function does (memory effects, norecurse, constant results, taint summaries). Second, transformations across function boundaries that use them: inlining and its cost models, cloning and outlining, interprocedural constant propagation, signature changes (dead arguments, argument promotion), global optimization, devirtualization, tail-recursion elimination — and the build modes that give them the whole program: monolithic LTO, ThinLTO, profile-guided optimization and post-link layout. In LLVM 23 these are the CGSCC inliner, function-attrs, ipsccp, deadargelim, argpromotion, globalopt, wholeprogramdevirt, pgo-icall-prom, tailcallelim and the lto / thinlto pipelines; in pebblec your print<pebble-callgraph>, pebble-inline, pebble-funcattrs and pebble-tre run on them.

What you will be able to do

  • Build CHA, RTA and XTA call graphs by hand and in code, prove them sound, and measure how much each removes against a dynamic call graph.
  • Run Tarjan's algorithm exactly as LLVM's scc_iterator does, explain bottom-up and top-down traversals, and state the invariants of the CGSCC pass manager under call-graph mutation.
  • Compute inline costs and decisions under a size threshold, a cost/benefit model with bonuses, and a profile, and explain why the problem is NP-hard and what MLGO learns.
  • Run IFDS tabulation by hand, prove its \(O(ED^3)\) bound (for call relations with \(O(D)\) edges), and say when summaries beat call strings.
  • Infer memory effects and norecurse over SCCs and prove the fixed point sound; decide dead arguments, argument promotion and GlobalOpt rewrites.
  • Transform tail recursion into loops with accumulators and prove it correct; decide when a call may be a sibling call.
  • Explain monolithic LTO, ThinLTO's summaries and import decisions, instrumentation and sampling PGO, and post-link layout, and read their output (llvm-lto2 -save-temps, llvm-profdata, -Rpass).
  • Implement print<pebble-callgraph>, pebble-inline, pebble-funcattrs and pebble-tre and pass ./course test 20; find where LLVM 23 and GCC 15 implement each technique.

Prerequisites: Ch 12 (writing passes, the new pass manager, pebble-bbcount counters), Ch 14 (lattices, least fixed points, MOP/MFP, the IFDS preview), Ch 15 (DFS, SCCs, dominators), Ch 16 (SSA and phis), Ch 17 (SCCP), Ch 19 (points-to sets, capture, mod/ref).

Notation

Shared notation follows the house notation: §1 (sets, functions, logic), §2 (orders and lattices), §3 (graphs), §7 (dataflow). Orientation: summaries and points-to sets grow up (\(\sqsubseteq\)), fixed points are least fixed points; for call graphs, \(G_1 \sqsubseteq G_2\) means \(G_1\) is at least as precise. In this chapter:

Symbol Meaning
\(F\), \(\mathit{CS}\), \(\mathrm{body}(c)\) functions (bodies), call sites, the body containing call site \(c\) (Definition 20.1.1)
\(G = (R, T)\), \(\lvert G \rvert\) a call graph: reachable bodies and targets per call site; its number of edges (Definition 20.1.2)
\(\mathrm{Dyn}\) the edges taken by some execution (Definition 20.1.1)
\(\mathit{ext}_{in}\), \(\mathit{ext}_{out}\) LLVM's external calling node and calls-external node (Algorithm 20.1.3)
\(C \preceq D\), \(\mathrm{sub}(D)\), \(\mathrm{lookup}(C, m)\), \(\mathrm{decl}(x)\) subclass order, subclasses, dispatch, declared type (Definition 20.1.4)
\(S[B]\), \(S[@g]\) XTA class sets of a body and of a global (Definition 20.1.7)
\(X_1, \dots, X_k\) SCCs in bottom-up order (Definition 20.2.2)
\(\mathrm{sum}(f)\), \(\mathrm{body}_f\) a function's summary and its summary equation (Algorithm 20.2.5)
\(b(e)\), \(\Delta(e)\), \(B\) benefit and size growth of inlining edge \(e\), size budget (Definition 20.3.3)
\(T\), cost inline threshold and cost of a call site (Definitions 20.3.6–20.3.8)
\(\varphi_c\), \(g_\varphi\) constant facts of call site \(c\); the clone of \(g\) specialized for \(\varphi\) (Definition 20.4.1)
\(b(w)\), \(k\), \(\ell\), \(\kappa_c\), \(\kappa_f\) outlining benefit of a sequence of length \(\ell\) occurring \(k\) times; call and function overheads (Definition 20.4.6)
\(D\), \(0\), \(\langle n, d \rangle\) IFDS fact domain, the zero fact, a node of the exploded supergraph (Definitions 20.5.2–20.5.3)
\(E\), \(N\) supergraph edges and nodes in complexity statements (Theorem 20.5.6)
none \(<\) read \(<\) write the memory-effect lattice (Definition 20.6.1)
\(\mathrm{tid}(T)\), \(\mathrm{Compat}(T)\) type identifier, compatible vtable address points (Definition 20.7.1)
\(\oplus\), \(e\), \(acc\) accumulator operation, its identity, the accumulator phi (Definition 20.8.3)
\(L\), \(\alpha\) ThinLTO import limit and evolution factor (Algorithm 20.9.5)
\(c(e)\), \(N_g\), \(n_k\) edge count, entry count of \(g\), count of call site \(k\) (Definition 20.10.1, Theorem 20.10.4)
\(\Phi\) weighted distance of a function order (Algorithm 20.10.6)

Numbered statements are N.k.m (chapter, lesson, counter), as in NOTATION.md §9.

Technique map

Family Techniques (origin) Lesson
Call-graph construction Direct call graphs (LLVM CallGraph, LazyCallGraph); class hierarchy analysis, CHA (Dean, Grove & Chambers 1995); rapid type analysis, RTA (Bacon & Sweeney 1996); XTA (Tip & Palsberg 2000) and VTA (Sundaresan et al. 2000); points-to-based, on-the-fly call graphs (Andersen-style; Lhoták & Hendren 2003) 20.1
SCCs and traversal Tarjan's SCC algorithm (Tarjan 1972) as scc_iterator; bottom-up and top-down traversal (Sharir & Pnueli 1981); the CGSCC pass manager over LazyCallGraph (LLVM) 20.2
Inlining Classic heuristics: knapsack formulation (Scheifler 1977), size thresholds, benefit/cost priorities (Davidson & Holler 1992; Ayers, Schooler & Gottlieb 1997; GCC badness); LLVM's InlineCost and inline advisor; ML-guided inlining, MLGO (Trofin et al. 2021) 20.3
Specialization and outlining Procedure cloning (Cooper, Hall & Kennedy 1993) and LLVM function specialization; partial inlining (LLVM PartialInliner, GCC ipa-split); outlining by suffix trees (Fraser, Myers & Wendt 1984; LLVM MachineOutliner, IROutliner) and hot/cold splitting (Pettis & Hansen 1990; LLVM HotColdSplitting) 20.4
Interprocedural analysis Interprocedural constant propagation with jump functions (Callahan, Cooper, Kennedy & Torczon 1986) and IPSCCP (after Wegman & Zadeck 1991); summary-based vs call-string analysis (Sharir & Pnueli 1981); IFDS (Reps, Horwitz & Sagiv 1995) and IDE (Sagiv, Reps & Horwitz 1996) 20.5
Attributes and signatures Attribute inference as an SCC fixed point (side effects after Banning 1979 and Cooper & Kennedy 1988; LLVM FunctionAttrs); dead-argument elimination; argument promotion; global optimization (LLVM deadargelim, argpromotion, globalopt) 20.6
Devirtualization Whole-program devirtualization over type metadata (LLVM WholeProgramDevirt); speculative devirtualization and indirect-call promotion (type feedback, Hölzle & Ungar 1994; GCC; LLVM pgo-icall-prom) 20.7
Tail calls Tail-call elimination (Steele 1977; sibling calls); tail-recursion elimination with accumulators (Burstall & Darlington 1977; LLVM tailcallelim, GCC tailr) 20.8
Whole-program compilation Monolithic LTO (Lattner & Adve 2004); ThinLTO (Johnson, Amini & Li 2017) 20.9
Profile-guided optimization Instrumentation PGO (Knuth & Stevenson 1973; Ball & Larus 1994; LLVM IR PGO, CSPGO); sampling PGO, AutoFDO (Chen, Li & Moseley 2016); post-link layout (Pettis & Hansen 1990; BOLT: Panchenko et al. 2019; Propeller: Shen et al. 2023) 20.10
flowchart LR
  D[Direct call graph] -->|resolve virtual calls by hierarchy| CHA[CHA 1995]
  CHA -->|only instantiated classes| RTA[RTA 1996]
  RTA -->|one set per method/global| XTA[XTA 2000]
  XTA -->|one set per variable| VTA[VTA 2000]
  VTA -->|allocation sites, on the fly| PTA[Points-to call graphs]
  PTA --> SCC[Tarjan SCCs, bottom-up / top-down]
  SCC --> SUM[Summaries: attributes, modref]
  SCC --> INL[Inlining: size, cost model, MLGO]
  SCC --> IPA[IPSCCP · IFDS/IDE]
  INL --> SPEC[Cloning · partial inlining · outlining]
  SUM --> SIG[DAE · argpromotion · GlobalOpt]
  CHA --> WPD[Whole-program devirtualization]
  WPD --> ICP[Speculative devirt / ICP]
  INL --> TRE[TCE · TRE with accumulators]
  LTO[Monolithic LTO] -->|summaries, parallel backends| THIN[ThinLTO]
  LTO --> INL
  PGO[Instrumentation PGO] -->|samples from production| AFDO[AutoFDO]
  PGO --> ICP
  AFDO --> POST[BOLT / Propeller]

Who uses what

System Technique Notes
LLVM 23 CallGraph/LazyCallGraph, CGSCC inliner with InlineCost (MLGO advisor when built with a model), function-attrs and rpo-function-attrs, IPSCCP with function specialization, partial inliner, machine/IR outliners, hot/cold splitting, deadargelim, argpromotion, globalopt, wholeprogramdevirt, pgo-icall-prom, tailcallelim, full and thin LTO, IR/CS/sample PGO, basic-block sections lessons 20.1–20.10, §7 of each
GCC 15 IPA call graph; ipa-inline (badness priority queue, unit growth); ipa-cp with jump functions and .constprop clones; ipa-split; ipa-pure-const and ipa-modref; ipa-devirt (type inheritance graph, speculative devirtualization); tailr; WHOPR LTO; -fprofile-use, -fauto-profile 20.3, 20.4, 20.6, 20.7, 20.9
Soot 4.6 CHA, RTA and VTA call graphs (Spark with rta/vta), on-the-fly Spark 20.1
WALA, PhASAR IFDS/IDE solvers (Java; LLVM IR) 20.5
HotSpot, V8 CHA without guards plus deoptimization; profile-driven inlining and type feedback 20.3, 20.7
BOLT, Propeller post-link block and function layout from sampled profiles 20.10
Pebble print<pebble-callgraph>, pebble-inline (size, cost, profile), pebble-funcattrs, pebble-tre; the lab's CHA, RTA and ★ XTA exercises, labs A and B

Comparison

The fixed columns follow docs/authoring/DEPTH_CONTRACT.md §3 item 8; each lesson repeats its own rows and defines its variables.

Call-graph construction: direct calls, CHA, RTA, XTA/VTA and points-to (lesson 20.1)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Direct call graph Exact for direct calls; every indirect call goes to "unknown" \(O(S)\) · one pass Sound without any whole-program assumption Low LLVM CallGraph/LazyCallGraph, GCC cgraph, every C compiler
CHA All subclasses of the declared type (21 edges on the example) \(O(S \lvert\mathcal{C}\rvert h)\) · fastest OO resolution Sound in a closed world Low (hierarchy + dispatch) Devirtualization in C++/Java compilers, LLVM WPD's type metadata, JIT class-hierarchy checks
RTA CHA ∩ instantiated classes (12) ~CHA · a few rounds Sound in a closed world; removes never-constructed classes Low GCC type_possibly_instantiated_p, Soot, whole-program Java tools
XTA / VTA Per-method (9) / per-variable (8) type sets \(O(\text{sets} \cdot \lvert\mathcal{C}\rvert \cdot S)\) · seconds on large Java programs Sound, flow-insensitive Medium (propagation graph) Soot vta, OPAL, WALA; GCC polymorphic call contexts (flow-sensitive cousin)
Points-to based Allocation sites, on the fly (6 = dynamic here) \(O(V^3)\) · the most expensive Sound; the precision of Ch 19's analysis High Soot Spark, Doop, SVF; indirect-call resolution in analyzers

SCCs and traversal orders: Tarjan, bottom-up and top-down, the CGSCC pass manager (lesson 20.2)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Tarjan's SCC algorithm All SCCs, bottom-up order, one pass \(O(n + e)\) · one DFS Deterministic given successor order (matters for reproducible pipelines) Low (iterative version: medium) scc_iterator, LazyCallGraph, GCC ipa_reduced_postorder, print<pebble-callgraph>
Bottom-up / top-down traversal Least fixed point of summary systems (Theorem 20.2.10); top-down for caller contexts one evaluation per non-recursive function Summaries are reusable (ThinLTO, caching) Low given SCCs FunctionAttrs, inliner, modref (bottom-up); IPSCCP args, rpo-function-attrs, ipa-cp (top-down)
CGSCC pass manager Bottom-up order under mutation (Proposition 20.2.11) near-linear, bounded devirt repeats Pass-level interleaving: each SCC fully simplified before its callers see it High (incremental SCCs, analysis invalidation) LLVM's default<O2> inliner pipeline

Inlining: heuristics, cost models and learned policies (lesson 20.3)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Classic heuristics (size threshold; GCC badness priority) Size: blind to call-site facts; priority: global benefit/growth order \(O(c)\) / \(O(c \log c)\) · cheap Predictable; GCC's dumps explain every decision Low / medium (needs summaries) -O1-style inliners, GCC's IPA inliner, pebble-inline<size>
LLVM InlineCost + advisor Per call site, simulates folding with the actual arguments; bonuses and profile thresholds \(O(\lvert g \rvert)\) per site, early exit · the dominant IPO cost at -O2 Remarks with cost and threshold (-pass-remarks=inline) High (every instruction kind, bonuses) LLVM default<O2>, pebble-inline<cost> / <profile=…>
MLGO Learned from whole-binary outcomes; not tied to one cost formula Model evaluation \(O(1)\) · features incremental Hard to explain an individual decision High (training infrastructure), low to deploy -Oz builds with a released model (Chrome, Android, Fuchsia toolchains)

Specialization and outlining: cloning, partial inlining, the outliners and hot/cold splitting (lesson 20.4)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Cloning / function specialization Per-call-site-class constants without inlining; turns indirect calls direct estimation \(O(s \lvert g \rvert)\) + re-solves · bounded clones Clones named .specialized.N / .constprop.N Medium (on top of IPSCCP) LLVM ipsccp<func-spec> (-O3, LTO), GCC ipa-cp
Partial inlining Inlines the hot entry path only \(O(\lvert g \rvert)\) · cheap .1.slow / .part.N functions Medium (code extraction) LLVM partial-inliner, GCC fnsplit
Outlining / hot/cold splitting Size: shares repeats (MachineOutliner, IROutliner); locality: moves cold code (HotColdSplitting) \(O(n)\) suffix tree / linear regions OUTLINED_FUNCTION_N, .cold.N High (liveness and call variants per target) / medium -Oz on AArch64/RISC-V (mobile), hot/cold with profiles; BOLT does the post-link version

Interprocedural analysis: IPSCCP, summaries vs contexts, IFDS and IDE (lesson 20.5)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
IPSCCP Constants and dead branches across calls; merges all call sites of a function (context-insensitive) near-linear · cheap, runs in default<O2> Replaced arguments and returns; poison for unused returns Medium on top of SCCP LLVM ipsccp, GCC ipa-cp
Summary-based vs context-sensitive Summaries: exact for distributive problems, reusable; call strings: exact only without recursion, \(C^k\) blow-up summaries once per function; call strings exponential in \(k\) Summaries are inspectable data (modref dumps, ThinLTO index) Low (summaries of simple lattices) to high (general call strings) FunctionAttrs, modref, ThinLTO (summaries); points-to (call strings, object sensitivity)
IFDS / IDE MVP exactly for distributive (IFDS) and linear-environment (IDE) problems \(O(E D^3)\), \(O(E D)\) separable · on-demand Facts per point plus the summary edges that explain them Medium (tabulation) to high (IDE micro-functions) Taint and security analyses (FlowDroid/Heros, PhASAR), typestate; rarely in optimizers

Attribute inference and signature transformations: FunctionAttrs, dead arguments, argument promotion, GlobalOpt (lesson 20.6)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Attribute inference Sound summaries (Theorem 20.6.6); SCC-optimistic; bottom-up norecurse is conservative \(O(n + e)\) · negligible Attributes in the IR, readable and checkable (-S) Low per attribute; high for the full set (captures, willreturn) LLVM function-attrs (3× in default<O2>), GCC ipa-pure-const, pebble-funcattrs
Dead-argument elimination Removes unused parameters/returns of internal functions linear · cheap New signatures; unchanged behavior Medium (rewriting all calls) LLVM deadargelim (module simplification, LTO); GCC IPA-SRA
Argument promotion By-pointer → by-value for read-only, dereferenceable arguments of internal functions linear + alias queries Parameters named %p.0.val Medium (safety checks) LLVM argpromotion in the CGSCC pipeline
Global optimization Constant, dead, split, shrunk and localized internal globals linear per round Fewer/simpler globals Medium (many rules) LLVM globalopt (twice in default<O2>, and in LTO)

Devirtualization: whole-program, speculative and profile-guided (lesson 20.7)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Whole-program devirtualization Exact single targets and constant results in a closed hierarchy; no guard linear in the LTO module · cheap Remarks per devirtualized call; relies on a programmer promise High (type metadata in the front end, LTO plumbing) Clang/LLVM with -flto -fwhole-program-vtables (Chromium, Android)
Speculative devirtualization / ICP Any likely target, open world; a guard keeps the fallback linear · a compare per promoted target Branch weights show the expected frequencies; wrong guesses only cost time Medium (profiles or a heuristic single target) GCC -fdevirtualize-speculatively (-O2), LLVM PGO/AutoFDO (pgo-icall-prom), JITs

Tail calls: tail-call elimination and tail-recursion elimination with accumulators (lesson 20.8)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Tail-call elimination Any call in tail position whose frame does not escape; ABI permitting \(O(n)\) · in the backend # TAILCALL in assembly; musttail makes failure a compile error Medium (per-target ABI checks) Every C/C++ compiler at -O2; mandatory in Scheme, Swift swifttailcc
Tail-recursion elimination with accumulators Self-recursion in tail position, or followed by one associative-commutative op \(O(n)\) · cheap A loop the IR optimizer can transform further Low–medium (phis, the accumulator, return rewriting) LLVM tailcallelim (in default<O2>), GCC tailr, pebble-tre

Whole-program compilation: monolithic LTO and ThinLTO (lesson 20.9)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Monolithic LTO Every interprocedural pass sees the whole program serial, \(O(P)\) memory · slow for large programs, no incrementality One module: -save-temps stages are easy to inspect Low for the compiler (reuses the pipeline), high for linkers Small and medium programs, release builds, when every percent counts
ThinLTO Whole-program decisions on summaries; transformations per module with imports thin link on the index + parallel, cached backends · near non-LTO build times Import and export decisions are printable (-print-imports, index dumps) High (summaries, promotion, index-based WPD) Large C/C++ code bases (Chromium, Android, Linux kernel with Clang), default LTO mode in many toolchains

Profile-guided optimization: instrumentation, sampling and post-link layout (lesson 20.10)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Instrumentation PGO Exact counts for the training run; value profiles; CSPGO adds post-inline contexts linear instrumentation · instrumented run markedly slower, separate training build Stale profiles are detected by CFG checksums and dropped with a warning Moderate (spanning tree, runtime, profile format) Release builds of compilers, browsers, databases; with LTO/ThinLTO
Sampling PGO (AutoFDO) Statistical, per source line; loses precision where optimization blurs line info linear annotation · no training build, negligible production overhead Staleness tolerated by line offsets; mismatches degrade silently High (debug-info mapping, inference, converters) Continuous FDO of data-center fleets, kernels
Post-link (BOLT, Propeller) Layout only (blocks, functions, splitting) but for the whole final binary including libraries near linear in binary size · BOLT: one rewrite; Propeller: relink Works on the binary actually shipped; BOLT needs relocations (--emit-relocs) Very high (binary rewriting; or compiler + linker co-design) Large, front-end-bound server binaries, compilers (on top of PGO+LTO)
Comparison-lab results (reproduce with the commands in the lab specs). Part A (ch20-cg --table --xta labs/ch20-callgraph/inputs/*.oo): call edges CHA 36, RTA 20, XTA 17 on the two hand-written programs (dynamic: 11); on 400 random programs CHA 5 649, RTA 4 869, XTA 4 048, VTA 3 211, dynamic 759 edges (Lesson 20.1 §5). Part B (measure.py --table, seven C benchmarks): executed calls 5 397 680 without inlining, 1 824 657 with the size threshold, 300 008 with the cost model, 8 with the profile; executed IR instructions −11 %, −16 % and −17 %, at +5 %, +5 % and +23 % static size (Lesson 20.3 §3).

Route through this chapter

Step What Techniques How it is exercised
1 Lesson 20.1 direct, CHA, RTA, XTA/VTA, points-to call graphs drill cha-rta; quiz; flashcards
2 Lesson 20.2 Tarjan, bottom-up/top-down, CGSCC drill scc-order; quiz
3 Exercises E1 and lab A Pebble implements the SCC order; CHA vs RTA (★ XTA) ./course test 20
4 Lesson 20.3, E2, lab B size, cost model, profile; MLGO drill inline-decision; tests; measurements
5 Lesson 20.4 cloning, partial inlining, outlining quiz computations
6 Lesson 20.5 IPSCCP, summaries, IFDS/IDE drill ifds-tabulation; quiz
7 Lesson 20.6, E3 attributes, DAE, argpromotion, GlobalOpt drill funcattrs-fixpoint; tests
8 Lesson 20.7 WPD, speculative devirtualization / ICP quiz
9 Lesson 20.8, E4 TCE, TRE (★ accumulators) tests
10 Lesson 20.9, Lesson 20.10 LTO, ThinLTO, PGO, AutoFDO, BOLT/Propeller real-world boxes; quiz
11 Theory test all ./course quiz 20 (≥ 80 % to finish)

Practice and check

./course drill scc-order --difficulty easy            # Tarjan as scc_iterator; --solution shows every step
./course drill cha-rta --difficulty medium            # CHA, RTA (hard: XTA) on a random program
./course drill inline-decision --difficulty hard      # cost, threshold, hot/cold sites
./course drill ifds-tabulation --difficulty medium    # path edges, summary edges, facts
./course drill funcattrs-fixpoint --difficulty medium # memory effects and norecurse over SCCs
./course flash 20                                     # daily, a few minutes
./course quiz 20                                      # after the lessons
./course test 20                                      # after the exercises and labs
./course status                                       # done = quiz ≥ 80 % and tests pass

References

The chapter's annotated bibliography — papers, textbook sections, pinned source files and docs — is in references.md. Start with: [GC01] (one framework for every call-graph construction of Lesson 20.1), [RHS95] (IFDS, the core of Lesson 20.5), [LLVM-InlineCost] (the production cost model behind Lesson 20.3), [JAL17] (ThinLTO's design and numbers).