build: prototype ThinLTO deadcode planning and size tuning - #2337
Draft
luoliwoshang wants to merge 6 commits into
Draft
build: prototype ThinLTO deadcode planning and size tuning#2337luoliwoshang wants to merge 6 commits into
luoliwoshang wants to merge 6 commits into
Conversation
luoliwoshang
force-pushed
the
codex/darwin-thinlto-slp
branch
from
August 15, 2026 14:56
564db6f to
a0a820e
Compare
LLGo baseline benchmarks
Program measurements
Core language and compiler benchmarks
Compared with |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prototype a complete ThinLTO-compatible deadcode pipeline for LLGo's Go
method-table pruning, then tune the Darwin LLVM 19 ThinLTO pipeline until the
result is smaller than the existing non-LTO
-deadcodedroppath in the testedprograms.
This PR is self-contained and based on
main. It includes:Os/Oz.The resulting model is:
LLGo remains responsible for Go-specific reachability. LLVM receives the
already-rewritten package modules and remains responsible for ThinLTO and
subsequent cross-module optimization.
Motivation
The existing non-ThinLTO
-deadcodedroppath emits same-name strong globals inthe entry module to override package-owned weak method tables. That mechanism
does not compose with ThinLTO's module summaries and symbol resolution.
With:
the strong-override experiment previously crashed LLVM 19.1.7 in:
The replacement global also lives outside the package module that owns the
original
weak_odrdefinition, COMDAT, and ThinLTO summary identity. Teachingthat override mechanism more ThinLTO special cases would preserve the wrong
ownership boundary.
This PR instead computes one global Go reachability plan, then applies the plan
inside each package module before that module's ThinLTO summary is written.
Design
Global planner
internal/deadcode.BuildPlanconsumes the merged package Meta summary and rootset and returns an explicit plan:
The current Meta analysis remains the source of Go-specific reachability facts.
The pipeline boundary does not require the current algorithm to remain fixed:
future work can add reflection facts, string-flow information, or other planner
inputs without restoring link-time strong overrides.
Package-level rewrite
internal/dcepass.RewriteTypeMethodTablesapplies the global plan to the LLVMmodule that owns each method table.
For dead method slots, it replaces
IFn/TFntargets withruntime.unreachableMethod. The original global stays in the original packagemodule and preserves its:
weak_odrlinkage;No same-name strong duplicate is emitted in the entry module for this mode.
Build integration and bitcode regeneration
The ThinLTO deadcode path is enabled only for:
Package LLVM modules are kept alive until
linkMainPkghas collected all Metaand built the link-specific plan.
materializeThinLTODeadcodethen:This ordering matters. Rewriting after summary emission would leave LLVM
analyzing stale edges: the summary could retain a method target that the IR had
already replaced with
runtime.unreachableMethod.The first prototype deliberately disables package-cache hits in this combined
mode. Cache overlays and immutable source bitcode are follow-up work.
ThinLTO import budget
LLVM's default ThinLTO import budget is performance-oriented. Imported bodies
also duplicate LLGo funcinfo entry sites. The combined ThinLTO deadcode mode
uses:
This retains very small cross-package imports while avoiding the text and
funcinfo growth observed with the default import budget.
Size optimization levels
ld64.lldaccepts numeric--lto-O0..3flags and rejects--lto-Os/Oz.LLGo now passes a linker optimization flag only for numeric levels.
OsandOzstill select the corresponding LLGo pre-link pipeline, while the linkeruses its supported default backend level.
Darwin ThinLTO SLP recovery
The K8s experiment exposed a separate LLVM 19 Mach-O LLD pipeline problem.
LLVM 19
PipelineTuningOptionsdefault to:ELF LLD explicitly enables both from the LTO optimization level, but LLVM 19
Mach-O LLD does not set
PTO.SLPVectorization. LLGo'sthinlto-pre-link<O2>pipeline intentionally defers SLP to the backend, soDarwin ThinLTO never runs the pass.
LLVM main now contains the missing Mach-O assignments:
For LLVM 19 compatibility, Darwin ThinLTO
O2,O3, andOspackagepipelines now append:
Linux, FullLTO, non-LTO,
O1, andOzpipelines are unchanged. Once LLGomoves to an LLVM version containing the upstream fix, post-link SLP is
preferable because it can also see imported code.
SLP root-cause evidence
The dominant K8s regression was:
The Go standard library embeds an 88,064-byte P-256 precomputed table. The
retained ThinLTO pre-link module contained:
Individual pass probes against the exact package bitcode produced:
nistec.initresultinstcombinememcpyoptvector-combineslp-vectorizer<16 x i8>vector storesdefault<O2>default<Os>default<O1>default<Oz>The real linker command contained
-flto=thinand--lto-O2. A single-job--lto-debug-pass-managertrace showed the complete O2 backend pipeline,including
LoopVectorizePass, but zeroSLPVectorizerPassexecutions.nistec.initstayed at 88,079 IR instructions through the backend.Without SLP, code generation emitted repeated
movplusstrb/strh/strinstructions. With SLP it emitted constant-pool
ldr qandstp qsequences.The function's estimated machine-code range fell from 767,116 bytes to 77,244
bytes.
Import budgets 0 and 5 produced the same 767,116-byte function before the SLP
fix, proving that cross-module importing was not the primary cause.
Size results
Environment for the final measurements:
Four demos
The baseline is non-ThinLTO without deadcode pruning. Existing DCE is the
current non-ThinLTO strong-override implementation. New is the complete pipeline
in this PR.
All four final binaries exited with status 0.
mimeheaderprinted the expectedhost value and the complete
gotypesdemo finished successfully.Single forced-build wall-time samples for the final binaries were 30.55 s,
24.46 s, 20.99 s, and 21.78 s respectively. These are diagnostic samples, not
reported as benchmark medians.
K8s workqueue
Benchmark source:
__textnistec.init__llgo_fie__LINKEDITCompared with ThinLTO+DCE before the import/SLP tuning:
__text: -946,036 bytes / -36.73%;nistec.init: -689,872 bytes / -89.93%.The final binary is 342,064 bytes (4.50%) smaller than the existing non-LTO
DCE binary. ThinLTO's
__llgo_fieremains larger, but its smaller__LINKEDITand restored text optimization more than compensate in this case.The K8s test binary still exits during startup with the existing:
The existing non-LTO DCE binary fails the same way. K8s is therefore currently
a build-size sample, not a runtime-correctness result.
Correctness validation
The package-owned rewrite path preserves linkage/COMDAT and has focused tests
for method-table initializer replacement. The ThinLTO combination also builds
and runs the interface/reflection cases used during the prototype:
A small interface experiment removed all three dead
Dropsymbols whilepreserving output:
__text0x51a40x506cDropsymbolsKnown limitations
write cached rewritten archives.
supported; the original method table must become immutable or reloadable.
MethodByNamestring/control-flow propagation is out of scope.Ozcontrols the LLGo pre-link pipeline, but an end-to-end size-orientedThinLTO backend mode is not available through LLVM 19
ld64.lld.toolchain includes the upstream Mach-O LTO fix.
Follow-ups
independently.
unreachable method calledK8s startupfailure before treating that benchmark as runtime validation.
Tests
Passed on the complete branch:
git diff --check upstream/main...HEADalso passes.