Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 106 additions & 6 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,25 @@
return buildenv.Dev && c.DeadcodeDrop && !c.goGlobalDCEEnabled()
}

// thinLTODeadcodeEnabled selects the experimental planner/rewrite path. The
// package archives are materialized only after the link-specific plan has been
// computed so their ThinLTO summaries describe the rewritten method tables.
func (c *Config) thinLTODeadcodeEnabled() bool {
return c != nil && c.deadcodeDropEnabled() && c.ltoMode() == lto.Thin
}

const thinLTODeadcodeImportLimitFlag = "-Wl,-mllvm,-import-instr-limit=5"

func thinLTODeadcodeLinkerArgs(c *Config) []string {
if !c.thinLTODeadcodeEnabled() {
return nil
}
// LLVM's default import budget is performance-biased. Imported bodies also
// duplicate LLGo funcinfo sites, so use the established size-oriented
// budget while retaining imports of very small cross-package callees.
return []string{thinLTODeadcodeImportLimitFlag}
}

func (c *Config) packageMetaEnabled() bool {
return c.CollectPackageMeta || c.deadcodeDropEnabled()
}
Expand Down Expand Up @@ -1108,7 +1127,12 @@
if err := ctx.collectFingerprint(aPkg); err != nil {
return err
}
ctx.tryLoadFromCache(aPkg)
// The first ThinLTO planner prototype needs the package LLVM modules alive
// until linkMainPkg computes the link-specific rewrite. Avoid consuming a
// prebuilt archive here; cache-aware bitcode overlays are a follow-up.
if !ctx.buildConf.thinLTODeadcodeEnabled() {
ctx.tryLoadFromCache(aPkg)
}
if verbose {
status := "MISS"
if aPkg.CacheHit {
Expand Down Expand Up @@ -1138,6 +1162,12 @@
if aPkg.CacheHit {
return packageBuildResultFor(task), nil
}
if ctx.buildConf.thinLTODeadcodeEnabled() {
if task.kind == cl.PkgLinkExtern {
appendExternalLinkArgs(ctx, aPkg, task.kindParam)
}
return packageBuildResultFor(task), nil
}
if err := normalizeToArchive(ctx, aPkg, verbose); err != nil {
return packageBuildResultFor(task), err
}
Expand Down Expand Up @@ -1214,7 +1244,7 @@
pkg := arg[:dot]
varName := arg[dot+1 : eq]
value := arg[eq+1:]
validateRewriteInput(pkg, varName, value)

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / build-cache (ubuntu-latest, 19)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / local_install_full (ubuntu-latest)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / local_install (ubuntu-latest)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / remote_install (ubuntu-latest)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / wasm-runtime (1.24.2)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / Dev Go Method Drop

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / llgo (ubuntu-latest, 19)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 19)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / llgo (primary, ubuntu-latest, LLVM 19, Go 1.26.5)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / Dev LTO GlobalDCE

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / cross-compile (ubuntu-latest, 19)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / test (primary, ubuntu-latest, LLVM 19, Go 1.26.5, shard 0)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / wasm-runtime (1.26.5)

undefined: packageBuildResultFor

Check failure on line 1247 in internal/build/build.go

View workflow job for this annotation

GitHub Actions / hello (primary, ubuntu-latest, LLVM 19, Go 1.26.5)

undefined: packageBuildResultFor
pkgs := []string{pkg}
if pkg == "main" {
pkgs = mainPkgs
Expand Down Expand Up @@ -1435,6 +1465,27 @@
linkArgs = append(linkArgs, rtLinkArgs...)
archiveInputs = append(archiveInputs, rtLinkInputs...)
}
if ctx.buildConf.thinLTODeadcodeEnabled() {
if err := materializeThinLTODeadcode(ctx, linkedOrder, needRuntime, verbose); err != nil {
return err
}
// The package archives are intentionally delayed in this mode so the
// rewritten module, rather than the original module, supplies the
// ThinLTO summary. Rebuild the package input list after materialization.
archiveInputs = archiveInputs[:0]
for _, aPkg := range linkedOrder {
if aPkg == nil || aPkg.ArchiveFile == "" {
continue
}
if isRuntimePkg(aPkg.PkgPath) {
if needRuntime || needPyInit || ctx.buildConf.Target == "" {
archiveInputs = append(archiveInputs, aPkg.ArchiveFile)
}
continue
}
archiveInputs = append(archiveInputs, aPkg.ArchiveFile)
}
}

// Generate main module file (needed for global variables even in library modes)
// This is compiled directly to .o and added to linkInputs (not cached)
Expand All @@ -1460,7 +1511,7 @@
funcInfo: funcInfo,
pcLineInfo: pcLineInfo,
})
if ctx.buildConf.deadcodeDropEnabled() {
if ctx.buildConf.deadcodeDropEnabled() && !ctx.buildConf.thinLTODeadcodeEnabled() {
if err := applyDeadcodeDropOverrides(linkedOrder, entryPkg, needRuntime, verbose); err != nil {
return err
}
Expand Down Expand Up @@ -1513,16 +1564,58 @@
return metas
}

func applyDeadcodeDropOverrides(pkgs []Package, entryPkg Package, needRuntime bool, verbose bool) error {
func buildDeadcodePlan(pkgs []Package, needRuntime bool) (deadcode.Plan, error) {
metas := linkedPackageMetas(pkgs)
summary, err := meta.NewGlobalSummary(metas)
if err != nil {
return deadcode.Plan{}, err
}
return deadcode.BuildPlan(summary, dceEntryRootCandidates(pkgs, needRuntime)), nil
}

func applyDeadcodeDropOverrides(pkgs []Package, entryPkg Package, needRuntime bool, verbose bool) error {
plan, err := buildDeadcodePlan(pkgs, needRuntime)
if err != nil {
return err
}
dcepass.EmitStrongTypeOverrides(entryPkg.LPkg.Module(), dceSourceModules(pkgs), plan.LiveSlots, verbose)
return nil
}

roots := dceEntryRootCandidates(pkgs, needRuntime)
liveSlots := deadcode.Analyze(summary, roots)
dcepass.EmitStrongTypeOverrides(entryPkg.LPkg.Module(), dceSourceModules(pkgs), liveSlots, verbose)
// materializeThinLTODeadcode applies the link-specific Go plan to each package
// module before its ThinLTO bitcode is emitted. Package cache overlays are
// deliberately out of scope for this first prototype; cache loading is
// disabled for thinLTODeadcodeEnabled above, so every package still owns its
// full LLVM module here.
func materializeThinLTODeadcode(ctx *context, pkgs []Package, needRuntime, verbose bool) error {
plan, err := buildDeadcodePlan(pkgs, needRuntime)
if err != nil {
return err
}
for _, aPkg := range pkgs {
if aPkg == nil || aPkg.LPkg == nil || aPkg.Package == nil {
continue
}
if aPkg.CacheHit {
return fmt.Errorf("thin LTO deadcode planner cannot rewrite cached package %s yet", aPkg.PkgPath)
}
dcepass.RewriteTypeMethodTables(aPkg.LPkg.Module(), plan.LiveSlots, verbose)
if aPkg.Package.ExportFile == "" {
continue
}
exportFile, exportBuffer, err := exportPackageObject(ctx, aPkg.PkgPath, aPkg.Package.ExportFile, aPkg.LPkg)
if err != nil {
return fmt.Errorf("export rewritten ThinLTO object of %s failed: %w", aPkg.PkgPath, err)
}
if exportFile != "" {
aPkg.ObjFiles = append(aPkg.ObjFiles, exportFile)
} else {
aPkg.ObjBuffers = append(aPkg.ObjBuffers, exportBuffer)
}
if err := normalizeToArchive(ctx, aPkg, verbose); err != nil {
return fmt.Errorf("archive rewritten ThinLTO object of %s failed: %w", aPkg.PkgPath, err)
}
}
return nil
}

Expand Down Expand Up @@ -1597,6 +1690,7 @@
return err
}
buildArgs = append(buildArgs, ltoPluginFlags...)
buildArgs = append(buildArgs, thinLTODeadcodeLinkerArgs(ctx.buildConf)...)

// Add build mode specific linker arguments
switch ctx.buildConf.BuildMode {
Expand Down Expand Up @@ -1992,6 +2086,12 @@
aPkg.LinkArgs = append(aPkg.LinkArgs, goCgoLinkArgs(ctx.buildConf.Goos, aPkg.AltPkg.Syntax)...)
}
if pkg.ExportFile != "" {
if ctx.buildConf.thinLTODeadcodeEnabled() {
if debugBuild || verbose {
fmt.Fprintf(os.Stderr, "==> Defer ThinLTO export %s: %s\n", aPkg.PkgPath, pkg.ExportFile)
}
return nil
}
exportFile, exportBuffer, err := exportPackageObject(ctx, pkg.PkgPath, pkg.ExportFile, ret)
if err != nil {
return fmt.Errorf("export object of %v failed: %v", pkgPath, err)
Expand Down
46 changes: 46 additions & 0 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,52 @@ func TestDeadcodeDropEnabled(t *testing.T) {
}
}

func TestThinLTODeadcodeEnabled(t *testing.T) {
tests := []struct {
name string
conf *Config
want bool
}{
{name: "not requested", conf: &Config{LTO: lto.Thin}, want: false},
{name: "thin lto", conf: &Config{DeadcodeDrop: true, LTO: lto.Thin}, want: buildenv.Dev},
{name: "lto off", conf: &Config{DeadcodeDrop: true, LTO: lto.Off}, want: false},
{name: "full lto", conf: &Config{DeadcodeDrop: true, LTO: lto.Full, DisableGoGlobalDCE: true}, want: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.conf.thinLTODeadcodeEnabled(); got != tt.want {
t.Fatalf("thinLTODeadcodeEnabled() = %v, want %v", got, tt.want)
}
})
}
}

func TestThinLTODeadcodeLinkerArgs(t *testing.T) {
tests := []struct {
name string
conf *Config
want []string
}{
{name: "not requested", conf: &Config{LTO: lto.Thin}},
{name: "lto off", conf: &Config{DeadcodeDrop: true, LTO: lto.Off}},
{name: "full lto", conf: &Config{DeadcodeDrop: true, LTO: lto.Full, DisableGoGlobalDCE: true}},
{name: "thin lto deadcode", conf: &Config{DeadcodeDrop: true, LTO: lto.Thin}, want: []string{thinLTODeadcodeImportLimitFlag}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
want := tt.want
if !buildenv.Dev {
want = nil
}
if got := thinLTODeadcodeLinkerArgs(tt.conf); !reflect.DeepEqual(got, want) {
t.Fatalf("thinLTODeadcodeLinkerArgs() = %v, want %v", got, want)
}
})
}
}

func TestPackageMetaEnabled(t *testing.T) {
tests := []struct {
name string
Expand Down
16 changes: 15 additions & 1 deletion internal/crosscompile/crosscompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le
"-Wl,--icf=none",
}
if ltoMode.Enabled() {
export.LDFLAGS = append(export.LDFLAGS, ltoMode.ClangFlag(), "-Wl,--lto"+level.Flag())
export.LDFLAGS = append(export.LDFLAGS, ltoMode.ClangFlag())
if flag := ltoLinkerOptFlag(level); flag != "" {
export.LDFLAGS = append(export.LDFLAGS, flag)
}
}
if clangRoot != "" {
clangLib := filepath.Join(clangRoot, "lib")
Expand Down Expand Up @@ -465,6 +468,17 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le
return
}

func ltoLinkerOptFlag(level optlevel.Level) string {
switch level {
case optlevel.O0, optlevel.O1, optlevel.O2, optlevel.O3:
return "-Wl,--lto" + level.Flag()
default:
// LLD's --lto-O option accepts only numeric levels. Clang likewise
// omits it for -Os/-Oz and lets the size-optimized IR drive LTO.
return ""
}
}

// UseTarget loads configuration from a target name (e.g., "rp2040", "wasi")
func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (export Export, err error) {
resolver := targets.NewDefaultResolver()
Expand Down
11 changes: 11 additions & 0 deletions internal/crosscompile/crosscompile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,17 @@ func TestDevLTOGlobalDCEUseLTOFlagsControlledByOption(t *testing.T) {
if !slices.Contains(thin.LDFLAGS, "-Wl,--lto-O2") {
t.Fatalf("missing thin LTO linker opt flag: %v", thin.LDFLAGS)
}
for _, level := range []optlevel.Level{optlevel.Os, optlevel.Oz} {
thinSize, err := use(runtime.GOOS, runtime.GOARCH, false, false, level, lto.Thin, false)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
for _, flag := range thinSize.LDFLAGS {
if strings.HasPrefix(flag, "-Wl,--lto-O") {
t.Fatalf("unexpected numeric-only LTO linker opt flag for %s: %v", level, thinSize.LDFLAGS)
}
}
}

full, err := use(runtime.GOOS, runtime.GOARCH, false, false, optlevel.O2, lto.Full, false)
if err != nil {
Expand Down
79 changes: 79 additions & 0 deletions internal/dcepass/dcepass.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,85 @@ func EmitStrongTypeOverrides(dst llvm.Module, srcMods []llvm.Module, liveSlots m
}
}

// RewriteTypeMethodTables rewrites ABI method table initializers in mod in
// place. It preserves each type descriptor's existing linkage and COMDAT, so
// ThinLTO sees one ordinary definition per package instead of an entry-module
// strong override competing with weak_odr definitions.
//
// A missing type entry in liveSlots means that no method slot is demanded.
// The method name and type are retained while IFn/TFn point at the runtime
// unreachable stub, preserving ABI table shape and method matching metadata.
func RewriteTypeMethodTables(mod llvm.Module, liveSlots map[string][]int, verbose bool) int {
if mod.IsNil() {
return 0
}
rewriter := &moduleRewriter{mod: mod}
rewritten := 0
for g := mod.FirstGlobal(); !g.IsNil(); g = llvm.NextGlobal(g) {
if g.IsDeclaration() || !g.IsGlobalConstant() {
continue
}
methodsVal, elemTy, ok := methodArray(g.Initializer())
if !ok {
continue
}
if rewriter.rewriteGlobal(g, methodsVal, elemTy, liveSlotSet(liveSlots[g.Name()]), verbose) {
rewritten++
}
}
return rewritten
}

type moduleRewriter struct {
mod llvm.Module
unreachable llvm.Value
}

func (r *moduleRewriter) unreachableMethod() llvm.Value {
if r.unreachable.IsNil() {
r.unreachable = r.mod.NamedFunction(unreachableMethodName)
}
if r.unreachable.IsNil() {
r.unreachable = llvm.AddFunction(r.mod, unreachableMethodName,
llvm.FunctionType(r.mod.Context().VoidType(), nil, false))
}
return r.unreachable
}

func (r *moduleRewriter) rewriteGlobal(g, methodsVal llvm.Value, elemTy llvm.Type, keepIdx map[int]bool, verbose bool) bool {
init := g.Initializer()
fields := make([]llvm.Value, init.OperandsCount())
for i := range fields {
fields[i] = init.Operand(i)
}
methods := make([]llvm.Value, methodsVal.OperandsCount())
dropped := false
for i := range methods {
orig := methodsVal.Operand(i)
if keepIdx[i] {
methods[i] = orig
continue
}
dropped = true
if verbose {
fmt.Fprintf(os.Stderr, "[dce] drop method %s[%d] ifn=%s tfn=%s\n", g.Name(), i, orig.Operand(2).Name(), orig.Operand(3).Name())
}
unreachable := r.unreachableMethod()
methods[i] = llvm.ConstNamedStruct(elemTy, []llvm.Value{
orig.Operand(0),
orig.Operand(1),
unreachable,
unreachable,
})
}
if !dropped {
return false
}
fields[len(fields)-1] = llvm.ConstArray(elemTy, methods)
g.SetInitializer(constStructOfType(init.Type(), fields))
return true
}

type overrideEmitter struct {
dst llvm.Module
values map[llvm.Value]llvm.Value
Expand Down
25 changes: 25 additions & 0 deletions internal/dcepass/dcepass_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dcepass
import (
"os"
"path/filepath"
"strings"
"testing"

qtest "github.com/qiniu/x/test"
Expand Down Expand Up @@ -47,6 +48,30 @@ func TestEmitStrongTypeOverrides(t *testing.T) {
}
}

func TestRewriteTypeMethodTablesPreservesLinkage(t *testing.T) {
ctx := llvm.NewContext()
defer ctx.Dispose()
mod := parseModule(t, &ctx, filepath.Join("testdata", "method_slots", "in.ll"))
defer mod.Dispose()

if got := RewriteTypeMethodTables(mod, map[string][]int{
taskTypeName: {1},
ptrTaskTypeName: {1},
}, false); got != 2 {
t.Fatalf("RewriteTypeMethodTables rewrote %d globals, want 2", got)
}
out := mod.String()
if !strings.Contains(out, `@_llgo_main.Task = weak_odr constant`) {
t.Fatalf("rewrite changed the source type linkage:\n%s", out)
}
if strings.Contains(out, `@_llgo_main.Task = constant`) {
t.Fatalf("rewrite introduced a strong duplicate:\n%s", out)
}
if !strings.Contains(out, `ptr @"github.com/goplus/llgo/runtime/internal/runtime.unreachableMethod"`) {
t.Fatalf("rewrite did not replace the dead method slot:\n%s", out)
}
}

func TestMethodArray(t *testing.T) {
ctx := llvm.NewContext()
defer ctx.Dispose()
Expand Down
Loading
Loading