diff --git a/src/cmd/compile/doc.go b/src/cmd/compile/doc.go index dee509218965f8..1c95f5a80bc4cb 100644 --- a/src/cmd/compile/doc.go +++ b/src/cmd/compile/doc.go @@ -86,6 +86,12 @@ Flags: object to usual output file (as specified by -o). Without this flag, the -o output is a combination of both linker and compiler input. + -llvm-lto + With the LLVM backend, emit package bitcode for FullLTO instead of + machine code. Required early IR lowering and LLVM's lto-pre-link + pipeline run before serialization; whole-program optimization and + code generation remain link-time work. With -llvm-keep-ir, .ll shows + prepared IR and .opt.ll shows the pre-link optimized IR in the archive. -m Print optimization decisions. Higher values or repetition produce more detail. diff --git a/src/cmd/compile/internal/base/flag.go b/src/cmd/compile/internal/base/flag.go index af24abd18d8198..e3eaa9dd59e557 100644 --- a/src/cmd/compile/internal/base/flag.go +++ b/src/cmd/compile/internal/base/flag.go @@ -129,7 +129,7 @@ type CmdFlags struct { PgoProfile string "help:\"read profile or pre-process profile from `file`\"" ErrorURL bool "help:\"print explanatory URL with error message if applicable\"" EnableLLVM bool "help:\"compile with the in-process LLVM backend\"" - LLVMLTO bool `flag:"llvm-lto" help:"emit LLVM bitcode for LTO at link time"` + LLVMLTO bool `flag:"llvm-lto" help:"emit pre-link optimized LLVM bitcode for LTO at link time"` LLVMKeepIR bool "flag:\"llvm-keep-ir\" help:\"keep frontend (.raw.ll), prepared (.ll), and optimized (.opt.ll) LLVM IR beside the output archive\"" LLVMOptPasses string "flag:\"llvm-opt-passes\" help:\"LLVM optimization pipeline (auto selects O0 with -N, O2 otherwise)\"" // Configuration derived from flags; not a flag itself. diff --git a/src/cmd/compile/internal/ssa/llvm_lto_test.go b/src/cmd/compile/internal/ssa/llvm_lto_test.go index 2f924237eb673c..dd761eba1fb786 100644 --- a/src/cmd/compile/internal/ssa/llvm_lto_test.go +++ b/src/cmd/compile/internal/ssa/llvm_lto_test.go @@ -12,6 +12,7 @@ import ( "internal/testenv" "os" "path/filepath" + "regexp" "strings" "testing" @@ -24,11 +25,11 @@ func TestLLVMSlimArchive(t *testing.T) { t.Run(arch, func(t *testing.T) { dir := t.TempDir() source := filepath.Join(dir, "p.go") - if err := os.WriteFile(source, []byte("package p\nfunc F(x int) int { return x + 1 }\n"), 0600); err != nil { + if err := os.WriteFile(source, []byte("package p\nfunc constant(x int) int { return 42 }\nfunc F(x int) int { return constant(x) }\n"), 0600); err != nil { t.Fatal(err) } output := filepath.Join(dir, "p.a") - cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "compile", "-enablellvm", "-llvm-lto", "-llvm-keep-ir", "-p=p", "-o", output, source) + cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "compile", "-enablellvm", "-llvm-lto", "-llvm-keep-ir", "-l", "-p=p", "-o", output, source) cmd.Env = append(cmd.Environ(), "GOOS=linux", "GOARCH="+arch) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("compile: %v\n%s", err, out) @@ -80,8 +81,39 @@ func TestLLVMSlimArchive(t *testing.T) { if !bytes.Contains(ir, []byte("!goobj.config")) { t.Fatal("pre-optimization IR lost GoObj configuration") } - if _, err := os.Stat(output + ".opt.ll"); !os.IsNotExist(err) { - t.Fatalf("LTO unexpectedly produced optimized IR: %v", err) + // Disable inlining above so this pure constant-returning call can only + // disappear through LLVM constant propagation and dead-call elimination in + // the package-level pipeline, before the archive is linked. + if !bytes.Contains(ir, []byte("call goabiinternal i64 @p.constant(")) { + t.Fatal("prepared IR has no call to exercise pre-link constant propagation") + } + if body := module.NamedFunction("p.F").String(); strings.Contains(body, "@p.constant(") || !strings.Contains(body, "ret i64 42") { + t.Fatal("serialized bitcode bypassed pre-link optimization") + } + optimized, err := os.ReadFile(output + ".opt.ll") + if err != nil { + t.Fatal(err) + } + if bytes.Contains(optimized, []byte("call goabiinternal i64 @p.constant(")) { + t.Fatal("optimized IR still contains the package-local call") + } + index := func(ir, name string) string { + t.Helper() + definition := regexp.MustCompile(`(?m)^define [^\n]*@` + regexp.QuoteMeta(name) + `\([^\n]*!goobj.symbol.index !([0-9]+)`) + ref := definition.FindStringSubmatch(ir) + if ref == nil { + t.Fatalf("missing indexed definition %s", name) + } + node := regexp.MustCompile(`(?m)^!` + ref[1] + ` = !\{([^\n]+)\}$`).FindStringSubmatch(ir) + if node == nil { + t.Fatalf("missing symbol index metadata for %s", name) + } + return node[1] + } + for _, name := range []string{"p.F", "p.constant"} { + if got, want := index(module.String(), name), index(string(ir), name); got != want { + t.Fatalf("pre-link optimization changed %s index from %s to %s", name, want, got) + } } // Export data must remain usable without asking the importer to read diff --git a/src/cmd/compile/internal/ssa/llvmbackend.go b/src/cmd/compile/internal/ssa/llvmbackend.go index 805e8ed24a85da..738a5372a86a10 100644 --- a/src/cmd/compile/internal/ssa/llvmbackend.go +++ b/src/cmd/compile/internal/ssa/llvmbackend.go @@ -17,8 +17,8 @@ import ( "github.com/goallc/go-llvm" ) -// EmitLLVMObject returns a GoObj linker object, or pre-optimization bitcode -// when -llvm-lto defers optimization and code generation to cmd/link. +// EmitLLVMObject returns a GoObj linker object, or pre-link optimized bitcode +// when -llvm-lto defers whole-program optimization and code generation to cmd/link. func EmitLLVMObject(outputFile string) ([]byte, error) { finalizeLLVMModule() if err := llvm.ConfigureGoObjFromModule(CurrentModule); err != nil { @@ -84,14 +84,11 @@ func EmitLLVMObject(outputFile string) ([]byte, error) { } } - if base.Flag.LLVMLTO { - buffer := llvm.WriteBitcodeToMemoryBuffer(CurrentModule) - defer buffer.Dispose() - return append([]byte(nil), buffer.Bytes()...), nil - } - pipeline := strings.TrimSpace(base.Flag.LLVMOptPasses) - if pipeline == "auto" { + if base.Flag.LLVMLTO { + // Use LLVM's pre-link policy, leaving whole-program decisions to LTO. + pipeline = "lto-pre-link" + } else if pipeline == "auto" { pipeline = "default" if base.Flag.N != 0 { pipeline = "default" @@ -110,6 +107,15 @@ func EmitLLVMObject(outputFile string) ([]byte, error) { } } + if base.Flag.LLVMLTO { + if err := llvm.VerifyModule(CurrentModule, llvm.ReturnStatusAction); err != nil { + return nil, fmt.Errorf("verify LLVM module after pre-link optimization: %w", err) + } + buffer := llvm.WriteBitcodeToMemoryBuffer(CurrentModule) + defer buffer.Dispose() + return append([]byte(nil), buffer.Bytes()...), nil + } + if err := tm.RunPassPluginPreCodeGen(CurrentModule, llvm.ObjectFile, plugin); err != nil { return nil, fmt.Errorf("run GoALLC pre-codegen plugin: %w", err) }