Skip to content

[WIP] feat(size opt): Go linktime like DCE #1580

Draft
luoliwoshang wants to merge 131 commits intoxgo-dev:mainfrom
luoliwoshang:feat/gen-bcfiles-mode
Draft

[WIP] feat(size opt): Go linktime like DCE #1580
luoliwoshang wants to merge 131 commits intoxgo-dev:mainfrom
luoliwoshang:feat/gen-bcfiles-mode

Conversation

@luoliwoshang
Copy link
Copy Markdown
Contributor

@luoliwoshang luoliwoshang commented Jan 20, 2026

MVP for #1587


Directory Normal(KB) DCE(KB) Savings(KB)
./_demo/go/abimethod 1227 879 348
./_demo/go/async 256 200 56
./_demo/go/cabi 70 69 1
./_demo/go/checkfile 1734 1127 607
./_demo/go/commandrun 1353 936 417
./_demo/go/complex 88 88 0
./_demo/go/defer 71 71 0
./_demo/go/export 115 115 0
./_demo/go/gobuild 2789 2447 342
./_demo/go/gobuild-1389 1749 1421 328
./_demo/go/goimporter-1389 5152 4723 429
./_demo/go/goroutine 70 70 0
./_demo/go/gotime 345 288 57
./_demo/go/gotoken 1753 1143 610
./_demo/go/gotypes 3749 2981 768
./_demo/go/ifaceconv 73 73 0
./_demo/go/ifaceprom-1559 73 73 0
./_demo/go/issue1538 70 70 0
./_demo/go/issue1538-floatcvtuint-over 70 70 0
./_demo/go/logdemo 1731 1120 611
./_demo/go/mapclosure 98 97 1
./_demo/go/maphash 1753 1143 610
./_demo/go/math 88 88 0
./_demo/go/mimeheader 1194 849 345
./_demo/go/mkdirdemo 1751 1127 624
./_demo/go/netip 1945 1287 658
./_demo/go/osfile 509 392 117
./_demo/go/oslookpath 1753 1129 624
./_demo/go/oswritestring 1732 1124 608
./_demo/go/randcrypt 1735 1125 610
./_demo/go/randdemo 1766 1138 628
./_demo/go/readdir 1733 1398 335
./_demo/go/reflectfunc 1733 1123 610
./_demo/go/reflectindirect 893 719 174
./_demo/go/reflectmake 2152 1860 292
./_demo/go/reflectname-1412 1729 1102 627
./_demo/go/statefn 70 70 0
./_demo/go/sync 1737 1126 611
./_demo/go/sysexec 1753 1129 624
./_demo/go/texttemplate 2222 1922 300
./_demo/go/timedur 1729 1102 627
./_demo/go/timer 478 347 131
./_demo/py/callpy 50 50 0
./_demo/py/matrix 50 50 0
./_demo/py/max 50 50 0
./_demo/py/pi 49 49 0
./_demo/py/print 49 49 0
./_demo/py/statistics 50 50 0
./_demo/py/tensor 50 50 0
./_demo/c/asmcall 1729 1102 627
./_demo/c/asmfullcall 1730 1103 627
./_demo/c/cabi 87 87 0
./_demo/c/cabisret 70 70 0
./_demo/c/cargs 1729 1102 627
./_demo/c/catomic 70 69 1
./_demo/c/cexec 33 33 0
./_demo/c/cgofull 1758 1132 626
./_demo/c/concat 70 69 1
./_demo/c/cppintf 70 70 0
./_demo/c/cppmintf 70 70 0
./_demo/c/cppstr 73 73 0
./_demo/c/crand 1729 1102 627
./_demo/c/ctime 69 69 0
./_demo/c/fcntl 70 69 1
./_demo/c/genints 70 70 0
./_demo/c/getcwd 33 33 0
./_demo/c/hello 1713 1102 611
./_demo/c/helloc 33 33 0
./_demo/c/linkname 69 69 0
./_demo/c/llama2-c 88 88 0
./_demo/c/netdbdemo 70 70 0
./_demo/c/qsort 70 69 1
./_demo/c/setjmp 69 69 0
./_demo/c/stacksave 69 69 0
./_demo/c/syncdebug 823 389 434
./_demo/c/thread 70 70 0


package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}
llgo/_demo/aaa on  remotes/origin/feat/gen-bcfiles-mode:feat/gen-bcfiles-mode [$?] via 🐹 v1.24.10
❯ ls -lh .
total 5968
-rwxr-xr-x@ 1 zhangzhiyang  staff   1.2M Jan 29 14:50 dce
-rw-r--r--  1 zhangzhiyang  staff    72B Jan 29 14:49 main.go
-rwxr-xr-x@ 1 zhangzhiyang  staff   1.7M Jan 29 14:50 nodce

~30% binary size reduction on a simple Hello World (1.7MB → 1.2MB).


Background & Motivation

The primary binary size problem in LLGo is that abi.Method entries in type metadata cannot be eliminated by LLD's standard DCE. The root cause: global type descriptors contain method tables (abi.UncommonType.Methods) whose IFn/TFn fields 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)"]
Loading

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's goobj carries 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)"]
Loading

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"]
    end
Loading

Stage 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"]
    end
Loading

Stage 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 --> F
Loading

Stage 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"]
Loading

Key Packages

Package Role
ssa/ Emits __llgo_relocs metadata during compilation (interface usage, method tables, reflect, type refs)
cl/irgraph/ Builds a dependency graph from an LLVM module: direct calls, ref edges, and parsed reloc edges
cl/deadcode/ Reachability analysis: flood from roots, match interface method demands against concrete method tables
cl/dcepass/ LLVM pass that zeroes out unreachable abi.Method IFn/TFn pointers in type descriptors
internal/build/ Orchestrates the pipeline: per-package IR + graph, merge, deadcode, dcepass, link

Reloc Edge Types

Reloc Kind Go Equivalent Purpose
useIface R_USEIFACE Marks concrete→interface type conversions
useIfaceMethod R_USEIFACEMETHOD Records which interface methods are actually called
methodOff R_METHODOFF Maps method table entries (mtyp, ifn, tfn triples)
useNamedMethod R_USENAMEDMETHOD Tracks reflect.MethodByName("constant") calls
reflectMethod R_REFLECTMETHOD Flags dynamic reflect method lookups (conservatively keep all exported methods)
typeRef R_USETYPE Connects parent type descriptors to child types

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @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 (.bc) during the compilation process. This functionality is designed to assist with tooling and experimental work, such as future ThinLTO or reachability analysis, by providing access to the intermediate bitcode representation of compiled packages. The change integrates seamlessly with the existing LLVM IR (.ll) generation mechanism, ensuring that the default build behavior remains unaffected unless explicitly opted into.

Highlights

  • New Feature Flag: Introduced a new development flag, -gen-bcfiles, and a corresponding configuration option, Config.GenBC, to enable the emission of package .bc (bitcode) files.
  • Bitcode File Generation: Implemented the functionality to generate LLVM bitcode files (.bc) alongside the standard object (.o) outputs during the build process.
  • Integration with Existing LLVM IR Flow: Extended the existing GenLL flow, allowing for the optional emission of .bc files for debugging and inspection, in addition to .ll files, while still compiling to .o for linking.
  • Opt-in Behavior: Ensured that the default build behavior remains unchanged; .bc file emission is an opt-in feature intended for tooling and experimental purposes, such as future ThinLTO or reachability analysis.

🧠 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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@luoliwoshang luoliwoshang marked this pull request as draft January 20, 2026 09:44
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/build/build.go Outdated
Comment on lines 1193 to 1214
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
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

@fennoai
Copy link
Copy Markdown
Contributor

fennoai Bot commented Jan 20, 2026

Critical Issue: Missing Config Field

The PR does not compile. The code references ctx.buildConf.GenBC but the GenBC field is missing from the Config struct definition in internal/build/build.go (lines 114-148).

Required fix: Add this field to the Config struct (after line 132):

GenBC         bool // generate pkg .bc files

This is blocking - must be fixed before merge.

Comment thread internal/build/build.go
if err := copyFileAtomic(f.Name(), llFile); err != nil {
return "", err
if ctx.buildConf.GenLL {
llFile := exportFile + ".ll"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: When both -gen-llfiles and -gen-bcfiles are enabled, the .ll file is read from disk twice:

  1. Line 1200: copyFileAtomic(f.Name(), llFile) reads the entire file
  2. 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.

Comment thread internal/build/build.go Outdated
// 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment accuracy: The comment says "keep copies of the .ll/.bc" but the actual behavior differs:

  • .ll files are copied from the temp file (line 1200)
  • .bc files 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

Comment thread internal/build/build.go Outdated
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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ...
}

Comment thread internal/build/build.go
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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
    }
}

Comment thread cmd/internal/flags/flags.go Outdated
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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. .bc files are LLVM bitcode (not immediately obvious to all users)
  2. They're for debugging/inspection purposes (matching the PR's stated use case)

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 20, 2026

Codecov Report

❌ Patch coverage is 75.46362% with 172 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.15%. Comparing base (ebf66ae) to head (225627e).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
cl/irgraph/graph.go 77.29% 31 Missing and 21 partials ⚠️
cl/deadcode/deadcode.go 73.56% 38 Missing and 8 partials ⚠️
cl/dcepass/dcepass.go 70.96% 21 Missing and 6 partials ⚠️
cl/instr.go 22.22% 18 Missing and 3 partials ⚠️
ssa/interface.go 74.41% 6 Missing and 5 partials ⚠️
ssa/package.go 85.33% 6 Missing and 5 partials ⚠️
ssa/abitype.go 93.33% 3 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@luoliwoshang luoliwoshang changed the title feat: add gen-bcfiles output option [WIP] feat: Go linktime like DCE Jan 21, 2026
@luoliwoshang luoliwoshang changed the title [WIP] feat: Go linktime like DCE [WIP] feat: Go linktime like DCE (BC analysis) Jan 25, 2026
luoliwoshang and others added 12 commits January 29, 2026 02:21
- 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>
@luoliwoshang luoliwoshang marked this pull request as ready for review January 29, 2026 10:59
@fennoai
Copy link
Copy Markdown
Contributor

fennoai Bot commented Jan 29, 2026

Code Review Summary

Nice work on implementing Go-style link-time DCE for LLGo! The architecture with irgraphdeadcodedcepass is well-structured. The 0.5MB binary size reduction shown in the PR description is promising.

Key observations:

  • Good separation of concerns between graph building, reachability analysis, and IR rewriting
  • Pre-indexing relocs by owner in deadcode.go is a solid optimization
  • Comprehensive test coverage across the new packages

A few issues worth addressing are noted in the inline comments below.

Comment thread cl/dcepass/dcepass.go
//
// 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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Removing demoteToDecl and updating this comment to accurately describe what Apply does, OR
  2. Actually calling demoteToDecl if function body removal is intended

Comment thread cl/dcepass/dcepass.go Outdated
}
}

func demoteToDecl(mod llvm.Module, fn llvm.Value) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot remove this unused function!

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Removed the unused demoteToDecl function.

Changes made:

  • Removed the demoteToDecl function (lines 113-131)
  • Updated the Apply function's doc comment which incorrectly referenced this behavior

PR created: luoliwoshang#185

Comment thread cl/deadcode/deadcode.go
ReachableMethods map[irgraph.SymID]map[int]bool // methods confirmed reachable by iface calls
}

var verbose bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot sound's good! make this change!

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Added Options struct to pass Verbose flag instead of using global variable.

Changes made:

  • Added Options struct with Verbose field in cl/deadcode/deadcode.go
  • Updated Analyze() function signature to accept Options
  • Removed global verbose variable and SetVerbose() function
  • Updated all callers (internal/build/build.go, test files)

This improves thread safety when multiple goroutines call Analyze() concurrently.

PR updated: luoliwoshang#185

Comment thread cl/deadcode/deadcode.go
func (d *deadcodePass) flood() {
for len(d.queue) > 0 {
cur := d.queue[0]
d.queue = d.queue[1:]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:]

Comment thread cl/dcepass/dcepass.go Outdated
// 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/") ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot try remove this hardcode at dcepass!

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

⚠️ Please test thoroughly - without these skip patterns, DCE will now also process runtime/reflect type metadata. If the reachability analysis doesn't properly mark these types' methods as reachable, it could break runtime behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot create another pr for me!

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_reflect
    • internal/reflectlite
    • internal/abi
    • _llgo_iface$

The DCE now relies on reachability analysis to determine which methods to keep, rather than using fragile substring matching.

⚠️ Please test thoroughly - without these skip patterns, DCE will now also process runtime/reflect type metadata. If the reachability analysis doesn't properly mark these types' methods as reachable, it could break runtime behavior.

luoliwoshang and others added 2 commits January 29, 2026 11:12
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>
Comment thread cl/dcepass/dcepass.go
Comment on lines +68 to +72
// Match __llgo_relocs, __llgo_relocs.123, etc.
if name == "__llgo_relocs" || strings.HasPrefix(name, "__llgo_relocs.") {
toRemove = append(toRemove, g)
removeSet[name] = true
}
Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread cl/dcepass/dcepass.go Outdated
// 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/") ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

⚠️ Please test thoroughly - without these skip patterns, DCE will now also process runtime/reflect type metadata.

Comment thread cl/dcepass/dcepass.go Outdated
Comment on lines +144 to +148
// 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/") ||
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot try remove this hardcode at dcepass!

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Removed the hardcoded runtime/reflect skip patterns from clearUnreachableMethods.

Changes made:

  1. Removed hardcoded package name skip patterns:
    • runtime/
    • reflect/
    • _llgo_reflect
    • internal/reflectlite
    • internal/abi
    • _llgo_iface$
  2. Removed unused demoteToDecl function
  3. Updated the Apply function 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

⚠️ Please test thoroughly - without these skip patterns, DCE will now also process runtime/reflect type metadata. If the reachability analysis doesn't properly mark these types' methods as reachable, it could break runtime behavior.

- 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>
Comment thread cl/dcepass/dcepass.go Outdated
Comment on lines +139 to +143
keepByName := isErrorLike(orig)
if keepByIdx || keepByName {
newMethods[i] = orig
continue
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot try to remove the ErrorLike keeplive ,i think current keeplive is sufficient

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Removed the isErrorLike keepalive pattern.

Changes made:

  • Removed isErrorLike function (lines 241-261)
  • Removed methodName function (lines 194-239) - only used by isErrorLike
  • Simplified clearUnreachableMethods to only check keepIdx
  • Updated test expected output

PR created: luoliwoshang#188

The DCE now relies solely on the reachability analysis to determine which methods to keep.

fennoai Bot and others added 2 commits January 30, 2026 11:27
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>
@luoliwoshang luoliwoshang force-pushed the feat/gen-bcfiles-mode branch from 1764932 to 225627e Compare January 30, 2026 06:44
@luoliwoshang luoliwoshang marked this pull request as draft February 9, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant