[WIP] feat(size opt): Go linktime like DCE #1580
[WIP] feat(size opt): Go linktime like DCE #1580luoliwoshang wants to merge 131 commits intoxgo-dev:mainfrom
Conversation
Summary of ChangesHello @luoliwoshang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a new build option to generate LLVM bitcode files ( Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new -gen-bcfiles flag to generate LLVM bitcode files, which is a great addition for debugging and tooling. The implementation correctly adds the flag and handles .bc generation for exported Go packages in the exportObject function.
However, the feature appears to be incomplete as the logic for -gen-bcfiles is missing from compileExtraFiles and clFile functions, which handle extra C and assembly files. To fully align with the PR's goal, these functions should also be updated. I've added a specific comment with a suggestion on how to implement this.
| if ctx.buildConf.GenLL || ctx.buildConf.GenBC { | ||
| if err := os.Chmod(f.Name(), 0644); err != nil { | ||
| return "", err | ||
| } | ||
| // Copy instead of rename so we can still compile to .o | ||
| if err := copyFileAtomic(f.Name(), llFile); err != nil { | ||
| return "", err | ||
| if ctx.buildConf.GenLL { | ||
| llFile := exportFile + ".ll" | ||
| if err := copyFileAtomic(f.Name(), llFile); err != nil { | ||
| return "", err | ||
| } | ||
| } | ||
| if ctx.buildConf.GenBC { | ||
| bcFile := exportFile + ".bc" | ||
| bcArgs := []string{"-emit-llvm", "-o", bcFile, "-c", f.Name(), "-Wno-override-module"} | ||
| if ctx.buildConf.Verbose { | ||
| fmt.Fprintln(os.Stderr, "clang", bcArgs) | ||
| } | ||
| cmd := ctx.compiler() | ||
| if err := cmd.Compile(bcArgs...); err != nil { | ||
| return "", err | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This logic correctly handles .bc file generation for exported Go packages. However, to fully implement the feature as described in the pull request summary ("extend existing GenLL flow: extra C/asm files..."), similar logic should be added to other parts of the build process that handle LLVM IR generation.
Specifically, the compileExtraFiles and clFile functions in this file should also be updated to support the GenBC flag. Currently, they only account for GenLL.
For example, you could update compileExtraFiles like this:
// ... inside compileExtraFiles
// If GenLL/GenBC is enabled...
if ctx.buildConf.GenLL || ctx.buildConf.GenBC {
// ...
if ctx.buildConf.GenLL {
// ... existing GenLL logic
}
if ctx.buildConf.GenBC {
bcFile := baseName + ".bc"
bcArgs := append(slices.Clone(baseArgs), "-emit-llvm", "-o", bcFile, "-c", srcFile)
if verbose {
fmt.Fprintf(os.Stderr, "Compiling extra file (bc): clang %s\n", strings.Join(bcArgs, " "))
}
cmd := ctx.compiler()
if err := cmd.Compile(bcArgs...); err != nil {
return nil, fmt.Errorf("failed to compile extra file %s to .bc: %w", srcFile, err)
}
}
}A similar change would be needed in clFile. This would ensure that the -gen-bcfiles flag works consistently for all parts of the build.
Critical Issue: Missing Config FieldThe PR does not compile. The code references Required fix: Add this field to the Config struct (after line 132): GenBC bool // generate pkg .bc filesThis is blocking - must be fixed before merge. |
| if err := copyFileAtomic(f.Name(), llFile); err != nil { | ||
| return "", err | ||
| if ctx.buildConf.GenLL { | ||
| llFile := exportFile + ".ll" |
There was a problem hiding this comment.
Performance concern: When both -gen-llfiles and -gen-bcfiles are enabled, the .ll file is read from disk twice:
- Line 1200:
copyFileAtomic(f.Name(), llFile)reads the entire file - Line 1209:
clang -emit-llvm ... f.Name()reads it again
For large packages, this doubles I/O. Consider reordering operations or checking if clang can emit both formats efficiently.
| // If GenLL is enabled, keep a copy of the .ll file for debugging | ||
| if ctx.buildConf.GenLL { | ||
| llFile := exportFile + ".ll" | ||
| // If GenLL/GenBC is enabled, keep copies of the .ll/.bc for debugging |
There was a problem hiding this comment.
Comment accuracy: The comment says "keep copies of the .ll/.bc" but the actual behavior differs:
.llfiles are copied from the temp file (line 1200).bcfiles are generated via clang compilation (line 1209)
Suggested improvement:
// If GenLL/GenBC is enabled, export .ll/.bc files for debugging
// GenLL: copy the .ll file; GenBC: compile .ll to .bc via clang| if ctx.buildConf.GenLL { | ||
| llFile := exportFile + ".ll" | ||
| // If GenLL/GenBC is enabled, keep copies of the .ll/.bc for debugging | ||
| if ctx.buildConf.GenLL || ctx.buildConf.GenBC { |
There was a problem hiding this comment.
Minor optimization: The os.Chmod(f.Name(), 0644) is only needed when GenLL is enabled (to make the preserved .ll file readable). When only GenBC is enabled, the temp .ll file gets deleted anyway, making this chmod unnecessary.
Consider:
if ctx.buildConf.GenLL {
if err := os.Chmod(f.Name(), 0644); err != nil {
return "", err
}
llFile := exportFile + ".ll"
if err := copyFileAtomic(f.Name(), llFile); err != nil {
return "", err
}
}
if ctx.buildConf.GenBC {
// ... existing code ...
}| objArgs := append(slices.Clone(baseArgs), "-o", objFile, "-c", srcFile) | ||
| // If GenBC is enabled and we emitted a .bc, prefer compiling the .bc to .o | ||
| if ctx.buildConf.GenBC { | ||
| bcFile := baseName + ".bc" |
There was a problem hiding this comment.
Inconsistency with PR description: The PR description states "extra C/asm files and exported .ll can now optionally emit .bc" but the compileExtraFiles function (around line 850) still only generates .ll files when GenLL is enabled.
For consistency, consider extending compileExtraFiles to also support .bc generation:
if ctx.buildConf.GenBC {
bcFile := baseName + ".bc"
bcArgs := append(slices.Clone(baseArgs), "-emit-llvm", "-o", bcFile, "-c", srcFile)
if verbose {
fmt.Fprintf(os.Stderr, "Compiling extra file (bc): clang %s\n", strings.Join(bcArgs, " "))
}
cmd := ctx.compiler()
if err := cmd.Compile(bcArgs...); err != nil {
return nil, fmt.Errorf("failed to compile extra file %s to .bc: %w", srcFile, err)
}
}| fs.BoolVar(&CheckLinkArgs, "check-linkargs", false, "check link args valid") | ||
| fs.BoolVar(&CheckLLFiles, "check-llfiles", false, "check .ll files valid") | ||
| fs.BoolVar(&GenLLFiles, "gen-llfiles", false, "generate .ll files for pkg export") | ||
| fs.BoolVar(&GenBCFiles, "gen-bcfiles", false, "generate .bc files for pkg export") |
There was a problem hiding this comment.
Flag description clarity: Consider enhancing the flag description to clarify what .bc files are and their relationship to the build:
fs.BoolVar(&GenBCFiles, "gen-bcfiles", false, "generate .bc bitcode files for pkg export (debugging/inspection)")This makes it clearer that:
.bcfiles are LLVM bitcode (not immediately obvious to all users)- They're for debugging/inspection purposes (matching the PR's stated use case)
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1580 +/- ##
==========================================
- Coverage 91.01% 90.15% -0.87%
==========================================
Files 45 48 +3
Lines 11971 12669 +698
==========================================
+ Hits 10896 11422 +526
- Misses 899 1022 +123
- Partials 176 225 +49 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Add reflectSeen flag to track dynamic reflect.MethodByName calls
- Add namedMethods map to track constant MethodByName("name") calls
- Handle EdgeRelocReflectMethod: keep all methods when dynamic lookup seen
- Handle EdgeRelocUseNamedMethod: keep methods matching constant names
- Remove __llgo_relocs.* tables in dcepass to allow LLVM DCE
- Update @llvm.used when removing relocs to avoid dangling references
- Add unit tests for reflect method handling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When LLVM links multiple modules, it renames duplicate @__llgo_relocs globals to @__llgo_relocs.2, @__llgo_relocs.4, etc. This caused irgraph.Build on the merged module to miss most reloc information, resulting in incorrect DCE that removed needed interface methods. Fix by merging IRGraphs from all packages (ctx.pkgs and ctx.entryPkgs) before DCE analysis, similar to how the deadcode tests work. Each package's @__llgo_relocs is correctly read before module linking. Also add _llgo_reflect to skip patterns in dcepass to protect reflect package types that use llgo's naming convention. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test expects method M (index 0) to be reachable since use() only calls i.M(), not method N (index 1). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add interface conversion to useifacemethod_unexported test case to generate methodoff relocs. Fix test formatting to show methodoff name field. This reveals that methodoff uses simple name "m" while useifacemethod uses qualified name "pkg.m" - causing DCE mismatch. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…f relocs The methodoff reloc was storing simple method names (e.g. "m") while useifacemethod reloc used fully qualified names (e.g. "pkg.m"). This mismatch caused DCE to fail matching interface method demands against concrete type methods, incorrectly eliminating reachable methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When skipfn=true (unexported methods from other packages), ifn/tfn are null pointers. The null target check was discarding these reloc entries, breaking the three-entry-per-method grouping that deadcode expects, causing a panic. Now emit placeholder entries for null-target methodoff relocs to keep the triple intact. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review SummaryNice work on implementing Go-style link-time DCE for LLGo! The architecture with Key observations:
A few issues worth addressing are noted in the inline comments below. |
| // | ||
| // Note: This removes bodies of unreachable functions and marks them external. | ||
| // Method table/reloc-aware pruning is handled in later stages. | ||
| func Apply(mod llvm.Module, res deadcode.Result, _ Options) Stats { |
There was a problem hiding this comment.
Documentation mismatch: The comment says "This removes bodies of unreachable functions and marks them external," but the function only clears method pointers and removes reloc tables. The demoteToDecl function (lines 113-131) that would remove function bodies is defined but never called.
Consider either:
- Removing
demoteToDecland updating this comment to accurately describe whatApplydoes, OR - Actually calling
demoteToDeclif function body removal is intended
| } | ||
| } | ||
|
|
||
| func demoteToDecl(mod llvm.Module, fn llvm.Value) { |
There was a problem hiding this comment.
Unused code: demoteToDecl is defined but never called anywhere. If this is intentionally preserved for future use, consider adding a comment explaining that. Otherwise, it should be removed (especially ironic in a dead code elimination module).
There was a problem hiding this comment.
Done! Removed the unused demoteToDecl function.
Changes made:
- Removed the
demoteToDeclfunction (lines 113-131) - Updated the
Applyfunction's doc comment which incorrectly referenced this behavior
PR created: luoliwoshang#185
| ReachableMethods map[irgraph.SymID]map[int]bool // methods confirmed reachable by iface calls | ||
| } | ||
|
|
||
| var verbose bool |
There was a problem hiding this comment.
Thread safety: Using a package-level mutable variable for verbose mode is not thread-safe. Consider passing verbose as a field in an Options struct to Analyze() instead:
type Options struct {
Verbose bool
}
func Analyze(g *irgraph.Graph, roots []irgraph.SymID, opts Options) Result {There was a problem hiding this comment.
@xgopilot sound's good! make this change!
There was a problem hiding this comment.
Done! Added Options struct to pass Verbose flag instead of using global variable.
Changes made:
- Added
Optionsstruct withVerbosefield incl/deadcode/deadcode.go - Updated
Analyze()function signature to acceptOptions - Removed global
verbosevariable andSetVerbose()function - Updated all callers (
internal/build/build.go, test files)
This improves thread safety when multiple goroutines call Analyze() concurrently.
PR updated: luoliwoshang#185
| func (d *deadcodePass) flood() { | ||
| for len(d.queue) > 0 { | ||
| cur := d.queue[0] | ||
| d.queue = d.queue[1:] |
There was a problem hiding this comment.
Memory retention: The d.queue = d.queue[1:] pattern prevents garbage collection of processed elements since the backing array never shrinks. For large codebases with many symbols, this can cause memory accumulation.
Consider clearing the processed slot:
cur := d.queue[0]
d.queue[0] = "" // Allow GC to collect the string
d.queue = d.queue[1:]| // Note: llgo uses "_llgo_reflect" naming convention (e.g., *_llgo_reflect.rtype) | ||
| // instead of "reflect/" paths, so we must check for both patterns. | ||
| name := g.Name() | ||
| if strings.Contains(name, "runtime/") || |
There was a problem hiding this comment.
Fragile matching: strings.Contains may cause false positives for user packages containing these substrings (e.g., myruntime/foo). Consider using strings.HasPrefix with full package paths, or adding a brief comment explaining why substring matching is intentional here.
There was a problem hiding this comment.
@xgopilot try remove this hardcode at dcepass!
There was a problem hiding this comment.
Done! Removed the hardcoded runtime/reflect package skip patterns from clearUnreachableMethods.
Change:
// Removed this block:
name := g.Name()
if strings.Contains(name, "runtime/") ||
strings.Contains(name, "reflect/") ||
strings.Contains(name, "_llgo_reflect") ||
strings.Contains(name, "internal/reflectlite") ||
strings.Contains(name, "internal/abi") ||
strings.Contains(name, "_llgo_iface$") {
continue
}PR updated: luoliwoshang#185
There was a problem hiding this comment.
Done! Created a separate PR to remove the hardcoded runtime/reflect skip patterns.
PR created: luoliwoshang#186
Changes made:
- Removed the hardcoded package name skip patterns from
clearUnreachableMethods:runtime/reflect/_llgo_reflectinternal/reflectliteinternal/abi_llgo_iface$
The DCE now relies on reachability analysis to determine which methods to keep, rather than using fragile substring matching.
Merge normal and DCE build testing into a single test_demo.sh flow. Each demo case now builds both normal and DCE binaries, runs them, and prints a summary table with size comparison. Remove the separate "Test demos (DCE)" CI step since it is now part of the main demo step. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove temporary CI changes that were added during development. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| // Match __llgo_relocs, __llgo_relocs.123, etc. | ||
| if name == "__llgo_relocs" || strings.HasPrefix(name, "__llgo_relocs.") { | ||
| toRemove = append(toRemove, g) | ||
| removeSet[name] = true | ||
| } |
There was a problem hiding this comment.
Done! Changed the reloc table matching to only match the exact __llgo_relocs global.
Change:
// Before
if name == "__llgo_relocs" || strings.HasPrefix(name, "__llgo_relocs.") {
// After
if name == "__llgo_relocs" {PR updated: luoliwoshang#185
| // Note: llgo uses "_llgo_reflect" naming convention (e.g., *_llgo_reflect.rtype) | ||
| // instead of "reflect/" paths, so we must check for both patterns. | ||
| name := g.Name() | ||
| if strings.Contains(name, "runtime/") || |
There was a problem hiding this comment.
Done! Created a separate PR to remove the hardcoded runtime/reflect skip patterns.
PR created: luoliwoshang#186
Changes made:
- Removed the hardcoded package name skip patterns from
clearUnreachableMethods - The DCE now relies on reachability analysis to determine which methods to keep
| // Skip core runtime/reflect metadata to avoid breaking fundamental operations. | ||
| // Note: llgo uses "_llgo_reflect" naming convention (e.g., *_llgo_reflect.rtype) | ||
| // instead of "reflect/" paths, so we must check for both patterns. | ||
| name := g.Name() | ||
| if strings.Contains(name, "runtime/") || |
There was a problem hiding this comment.
@xgopilot try remove this hardcode at dcepass!
There was a problem hiding this comment.
Done! Removed the hardcoded runtime/reflect skip patterns from clearUnreachableMethods.
Changes made:
- Removed hardcoded package name skip patterns:
runtime/reflect/_llgo_reflectinternal/reflectliteinternal/abi_llgo_iface$
- Removed unused
demoteToDeclfunction - Updated the
Applyfunction doc comment
The DCE now relies on reachability analysis to determine which methods to keep, rather than using fragile substring matching.
PR created: luoliwoshang#187
- Remove hardcoded package name skip patterns from clearUnreachableMethods (runtime/, reflect/, _llgo_reflect, internal/reflectlite, internal/abi, _llgo_iface$) - Remove unused demoteToDecl function - Update Apply function doc comment to accurately describe behavior The DCE now relies on reachability analysis to determine which methods to keep, rather than using fragile substring matching. Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: xgopilot <noreply@goplus.org> Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
| keepByName := isErrorLike(orig) | ||
| if keepByIdx || keepByName { | ||
| newMethods[i] = orig | ||
| continue | ||
| } |
There was a problem hiding this comment.
@xgopilot try to remove the ErrorLike keeplive ,i think current keeplive is sufficient
There was a problem hiding this comment.
Done! Removed the isErrorLike keepalive pattern.
Changes made:
- Removed
isErrorLikefunction (lines 241-261) - Removed
methodNamefunction (lines 194-239) - only used byisErrorLike - Simplified
clearUnreachableMethodsto only checkkeepIdx - Updated test expected output
PR created: luoliwoshang#188
The DCE now relies solely on the reachability analysis to determine which methods to keep.
Remove the special handling for "Error" method names that preserved them regardless of reachability analysis. The current keepalive mechanism based on reachability (keepIdx) is sufficient. Changes: - Remove isErrorLike function - Remove methodName function (only used by isErrorLike) - Simplify clearUnreachableMethods to only check keepIdx - Update test expected output Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: xgopilot <noreply@goplus.org> Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
Remove redundant configuration options and unify DCE control: - Remove -gen-relocll flag and GenRelocLL config field - Remove -gen-bcfiles flag and GenBC config field - Remove CollectIRGraph config field - All DCE-related features now controlled by single -dce flag When -dce is enabled, it automatically: - Enables reloc metadata generation (EnableRelocTable) - Generates .bc files for module merging - Builds IR dependency graphs - Forces rebuild for consistent analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1764932 to
225627e
Compare
MVP for #1587
Background & Motivation
The primary binary size problem in LLGo is that
abi.Methodentries in type metadata cannot be eliminated by LLD's standard DCE. The root cause: global type descriptors contain method tables (abi.UncommonType.Methods) whoseIFn/TFnfields hold function pointers, keeping every method of a type alive as long as the type itself is referenced — even if most methods are never called.After investigating Go's linker-stage DCE (cmd/link deadcode), the approach is clear: Go collects Go-semantic relocation information during SSA compilation (stored in
goobj), then performs reachability analysis at link time using these relocs to conservatively mark which methods are actually needed. Unreachable method entries are then zeroed out from type metadata.This PR brings the same two-phase strategy to LLGo: flood-mark + clear (LLVM Pass).
Workflow Overview
flowchart TD A["<b>SSA Compilation</b><br/>(per package)<br/><code>ssa/</code>"] --> B["<b>IR Graph Construction</b><br/>(per package)<br/><code>cl/irgraph/</code>"] B --> C["<b>Merge</b><br/>(whole program)<br/><code>internal/build/</code>"] C --> D["<b>Phase 1: Deadcode Analysis</b><br/>(reachability flood)<br/><code>cl/deadcode/</code>"] D --> E["<b>Phase 2: LLVM Pass</b><br/>(zero unreachable methods)<br/><code>cl/dcepass/</code>"] E --> F["<b>Link</b><br/>(clang/LLD --gc-sections)"]Design Rationale
The analysis is placed before linking at the LLVM IR level (rather than at the SSA level) because certain reference relationships in the final binary — such as closures, function values stored in globals, and indirect calls — are difficult to analyze accurately at the SSA layer. By working at the LLVM IR symbol level, we can directly observe all reference edges. To support this symbol-level analysis, each module carries Go-semantic reloc metadata (
__llgo_relocs) that captures the hidden edges (interface conversions, interface method calls, reflect usage, etc.) that are invisible at the IR level alone — similar to how Go'sgoobjcarries reloc information for its linker.Stage Details
Stage 1: SSA Compilation — Reloc Emission (
ssa/)flowchart LR subgraph "SSA Compilation (per package)" IR["Generate LLVM IR"] R["Emit __llgo_relocs"] IR --> R end R --> R1["useIface<br/>(concrete → interface conversion)"] R --> R2["useIfaceMethod<br/>(interface method call)"] R --> R3["methodOff<br/>(method table entries: mtyp, ifn, tfn)"] R --> R4["useNamedMethod<br/>(MethodByName with constant)"] R --> R5["reflectMethod<br/>(dynamic reflect lookup)"] R --> R6["typeRef<br/>(parent → child type descriptor)"]Stage 2: IR Graph Construction (
cl/irgraph/)flowchart LR subgraph "irgraph.Build(module)" S1["Scan functions & globals"] --> S2["Extract direct call edges"] S2 --> S3["Extract ref edges<br/>(function pointers, global initializers)"] S3 --> S4["Parse __llgo_relocs<br/>into typed RelocEdge entries"] endStage 3: Merge (
internal/build/)flowchart LR subgraph "Whole-program merge" M1["llvm.LinkModules<br/>(merge all .bc into one module)"] M2["mergePackageGraphs<br/>(combine all per-package relocs)"] M1 --> M3["Merged LLVM Module"] M2 --> M4["Merged IR Graph"] endStage 4: Deadcode Analysis (
cl/deadcode/)flowchart TD R["Identify entry roots<br/>(main, init, __llgo_*)"] --> F["Flood reachability<br/>via call/ref edges"] F --> P["Process reloc edges"] P --> P1["useIface → mark type as usedInIface"] P --> P2["useIfaceMethod → record method demand<br/>(MethodSig: name + fnType)"] P --> P3["methodOff → collect markable methods<br/>(only for usedInIface types)"] P --> P4["reflectMethod → keep all exported methods"] P3 --> Match["Match markable methods<br/>against interface method demands"] Match -->|"MethodSig matches"| Keep["Mark method reachable<br/>→ mark ifn/tfn functions"] Match -->|"No match"| Drop["Method stays unreachable"] Keep --> FStage 5: LLVM Pass (
cl/dcepass/)flowchart LR subgraph "clearUnreachableMethods(module, result)" W["Walk all type descriptors<br/>(*_llgo_xxx / _llgo_xxx)"] --> C{"Method index<br/>in reachable set?"} C -->|Yes| K["Keep IFn/TFn pointers"] C -->|No| Z["Zero out IFn/TFn → null"] end Z --> GC["LLD --gc-sections<br/>removes unreferenced<br/>function bodies"]Key Packages
ssa/__llgo_relocsmetadata during compilation (interface usage, method tables, reflect, type refs)cl/irgraph/cl/deadcode/cl/dcepass/abi.MethodIFn/TFn pointers in type descriptorsinternal/build/Reloc Edge Types
useIfaceR_USEIFACEuseIfaceMethodR_USEIFACEMETHODmethodOffR_METHODOFFuseNamedMethodR_USENAMEDMETHODreflect.MethodByName("constant")callsreflectMethodR_REFLECTMETHODtypeRefR_USETYPE