build: prototype package-level deadcode planning for ThinLTO - #2291
Draft
luoliwoshang wants to merge 2 commits into
Draft
build: prototype package-level deadcode planning for ThinLTO#2291luoliwoshang wants to merge 2 commits into
luoliwoshang wants to merge 2 commits into
Conversation
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
This PR prototypes a ThinLTO-compatible deadcode pipeline for LLGo's Go method-table pruning.
The main architectural change is to separate the work into three stages:
This keeps LLGo's language-specific reachability knowledge in the planner while handing the rewritten program to the standard ThinLTO pipeline. It also establishes a boundary where the current planner can be replaced or extended later without changing the package rewrite/link flow.
This is intentionally a prototype. It validates the architecture and size behavior first; it does not yet implement
MethodByNamestring propagation, archive/cache integration, or ThinLTO backend caching.Motivation
The existing
-deadcodedroppath materializes strong method-table overrides in the entry module. That approach works for the normal link pipeline, but it does not compose correctly with ThinLTO.With:
LLVM 19.1.7 previously crashed in:
The strong override is also an awkward long-term integration point: the replacement global is detached from the package module that owns the original
weak_odrglobal, its COMDAT, and its ThinLTO summary identity.The goal of this experiment is therefore not to teach the existing override mechanism about ThinLTO. Instead, it makes LLGo's planner produce a global liveness plan, applies that plan to the owning package modules, and then lets ThinLTO analyze the resulting modules normally.
Previous strong-override design
The old non-ThinLTO path remains available and unchanged.
It computes dead method slots and emits same-name strong globals in the entry module to override the package-owned weak method tables at link time. ThinLTO sees both the original package definition and the late replacement through its module-summary/global-resolution machinery. That mismatch is the source of the LLVM crash seen in this experiment.
The new ThinLTO path deliberately does not emit those entry-module overrides.
Proposed pipeline
When both
-lto=thinand-deadcodedropare enabled, the build now performs:The plan currently contains the live method slots for each type:
deadcode.BuildPlanuses the existing reachability analysis.deadcode.Analyzeis retained as a compatibility wrapper.The important boundary is that the package contributes Meta and owns its LLVM globals, while the planner makes one whole-program decision. The planner implementation can evolve independently; for example, later work could add reflection/string-flow facts or consume additional LLVM analysis without restoring the strong-override design.
Implementation
Global planning
internal/deadcodenow exposesBuildPlan, which converts the merged global Meta summary and root set into an explicit reusable plan.Package-local rewrite
internal/dcepass.RewriteTypeMethodTablesapplies the global plan to each package module in place.For dead method slots, it replaces
IFn/TFntargets withruntime.unreachableMethod. The original method-table global remains in its owning module, including its:weak_odrlinkage;No same-name strong duplicate is added to the entry module.
Build integration
The experimental path is enabled only for the combination:
For this mode, package bitcode/archive emission is delayed until all package Meta has been merged and the global plan is available. Each package module is rewritten first, then its ThinLTO bitcode and summary are generated from the rewritten IR.
Other build configurations continue to use their existing paths.
Size-level linker compatibility
ld64.lldaccepts numeric values for--lto-O0..3and rejects--lto-Oz.ltoLinkerOptFlagnow emits the numeric linker flag only forO0throughO3;OsandOzomit it, matching Clang driver behavior while retaining the size-oriented LLGo pre-link pipeline.Why rewrite before emitting ThinLTO summaries?
ThinLTO decisions are driven by per-module summaries. Rewriting after summary emission would leave LLVM analyzing stale references: the summary could claim a method target is live even though the IR was later changed to
runtime.unreachableMethod.Emitting the summary from the already-rewritten package module ensures that symbol resolution, importing, internalization, and backend DCE all see the same graph.
Correctness validation
The ThinLTO + deadcode combination now completes and runs correctly for the interface/reflection cases used to exercise method-table reachability:
A small interface experiment also confirms that pruning reaches the final binary while preserving behavior:
__text0x51a40x506cDropsymbolsProgram output is identical.
Four-demo size experiment
Environment:
All 12 final binaries exited with status 0, and every output matched the previous DCE reference byte for byte.
Binary size
The no-DCE baseline is the normal non-ThinLTO build. Previous DCE is the existing non-ThinLTO strong-override path. The two new columns use the package-level planner and ThinLTO rewrite.
A positive value means the first column in the comparison is larger.
mimeheaderis smaller than the previous DCE in both ThinLTO modes, butOzis larger than ThinLTOO2in all four demos.Build time
Each value is the median wall time (
real) of three sequential forced rebuilds for one demo. The compiler binary build is excluded. The rounds were ordered differently to reduce warm-cache and thermal-order effects.ThinLTO is currently slower because this prototype disables package cache in the ThinLTO + deadcode mode, delays package emission, rewrites every package module, regenerates its bitcode summary, and then performs the ThinLTO link.
Section attribution
The incremental
Ozsize is not explained by__textalone:__text__text__llgo_fie__llgo_fie__llgo_fiecontains the funcinfo entry-site records emitted inside function bodies. ThinLTO can duplicate those records through inline copies. The link-phasepclnpostrewrite deduplicates the logical table in place, but it does not shrink the already allocated Mach-O section, so its zero-filled tail still contributes to the file size. The Oz pipeline produced more such records than O2, which dominates the Oz size increase in these binaries.The final text-symbol counts also show that Oz is not simply retaining all code:
Interpretation
The planner itself still prunes method targets correctly. The size comparison is affected by three independent factors:
__llgo_fiesection capacity, and the in-place post-link rewrite cannot return that capacity to the Mach-O file.ld64.lldaccepts only numeric--lto-O0..3; there is no--lto-Ozbackend flag. The Oz experiment therefore appliesthinlto-pre-link<Oz>and size-oriented front-end IR optimization, while the ThinLTO backend keeps its numeric default optimization level.This means the current Oz result is a useful diagnostic experiment, not yet an end-to-end size-optimized ThinLTO mode. The next size experiments should tune ThinLTO import/inlining and funcinfo-section emission directly.
Known limitations
Ozcurrently controls the LLGo pre-link pipeline; an end-to-end size-oriented ThinLTO backend mode is not wired up.MethodByNamestring/control-flow propagation is out of scope for this experiment.Follow-ups
-Ozand size-oriented ThinLTO import/inlining thresholds.Tests
Passed:
The four-demo runtime/build experiment also passed with identical output for all 12 binaries.
A full
go test ./internal/buildrun was started but entered an existing long-running build test path and was interrupted after the targeted tests passed. The earlier full-suite attempt in this checkout also has the knowntest/goanalyzer panic,clmatrix segfault, andcltimeout described above.