Skip to content

Commit b0d59da

Browse files
committed
builder: add real ThinLTO mode
We use ThinLTO for linking, but we use it in a way that doesn't give most of its benefits: we merge all the bitcode files into a single LLVM module and run some optimizations on it before linking. Therefore, this works more like a traditional "full" LTO link rather than a true thin link. This commit adds a new experimental -lto=thin option to do a true ThinLTO link. The main benefit is that linking will be a lot faster, especially for large programs consisting of many packages. At the moment, it only works for programs that don't do interface type asserts and don't call interface methods. It also probably won't work on WebAssembly and baremetal systems. But it's part of a larger goal towards a truly incremental build system: #2870 Once interface type asserts and method calls are converted to a vtable-like implementation, most programs should just work on linux/darwin/windows.
1 parent 48bbb3b commit b0d59da

File tree

5 files changed

+48
-1
lines changed

5 files changed

+48
-1
lines changed

builder/build.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
175175
DefaultStackSize: config.StackSize(),
176176
NeedsStackObjects: config.NeedsStackObjects(),
177177
Debug: true,
178+
LTO: config.Options.LTO != "legacy",
178179
}
179180

180181
// Load the target machine, which is the LLVM object that contains all
@@ -609,8 +610,38 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
609610
},
610611
}
611612

613+
// Add job to create the runtime.initAll function in a new module.
614+
initAllJob := &compileJob{
615+
description: "create runtime.initAll",
616+
result: filepath.Join(tmpdir, "runtime-initAll.bc"),
617+
run: func(job *compileJob) (err error) {
618+
// Create module with runtime.initAll.
619+
ctx := llvm.NewContext()
620+
defer ctx.Dispose()
621+
initAllMod := createInitAll(ctx, config, compilerConfig, lprogram.Sorted())
622+
defer initAllMod.Dispose()
623+
624+
// Write module to bitcode file.
625+
llvmBuf := llvm.WriteThinLTOBitcodeToMemoryBuffer(initAllMod)
626+
defer llvmBuf.Dispose()
627+
return os.WriteFile(job.result, llvmBuf.Bytes(), 0666)
628+
},
629+
}
630+
612631
// Prepare link command.
613-
linkerDependencies := []*compileJob{outputObjectFileJob}
632+
var linkerDependencies []*compileJob
633+
switch config.Options.LTO {
634+
case "legacy":
635+
// Link all Go bitcode files together into a single large module and
636+
// then do a ThinLTO link with the resulting large module + extra C
637+
// files (from CGo etc).
638+
linkerDependencies = append(linkerDependencies, outputObjectFileJob)
639+
case "thin":
640+
// Do a real thin link, with each Go package in a separate translation
641+
// unit. This is faster than merging them into one big LTO module.
642+
linkerDependencies = append(linkerDependencies, packageJobs...)
643+
linkerDependencies = append(linkerDependencies, initAllJob)
644+
}
614645
result.Executable = filepath.Join(tmpdir, "main")
615646
if config.GOOS() == "windows" {
616647
result.Executable += ".exe"

compileopts/options.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var (
1414
validPrintSizeOptions = []string{"none", "short", "full"}
1515
validPanicStrategyOptions = []string{"print", "trap"}
1616
validOptOptions = []string{"none", "0", "1", "2", "s", "z"}
17+
validLTOOptions = []string{"legacy", "thin"}
1718
)
1819

1920
// Options contains extra options to give to the compiler. These options are
@@ -25,6 +26,7 @@ type Options struct {
2526
GOARM string // environment variable (only used with GOARCH=arm)
2627
Target string
2728
Opt string
29+
LTO string
2830
GC string
2931
PanicStrategy string
3032
Scheduler string
@@ -107,6 +109,10 @@ func (o *Options) Verify() error {
107109
}
108110
}
109111

112+
if !isInArray(validLTOOptions, o.LTO) {
113+
return fmt.Errorf("invalid -lto=%s: valid values are %s", o.LTO, strings.Join(validLTOOptions, ", "))
114+
}
115+
110116
return nil
111117
}
112118

compiler/compiler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Config struct {
5454
DefaultStackSize uint64
5555
NeedsStackObjects bool
5656
Debug bool // Whether to emit debug information in the LLVM module.
57+
LTO bool // non-legacy LTO (meaning: package bitcode is merged by the linker)
5758
}
5859

5960
// compilerContext contains function-independent data that should still be

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,7 @@ func main() {
13571357
command := os.Args[1]
13581358

13591359
opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z")
1360+
lto := flag.String("lto", "legacy", "LTO mode: legacy or thin")
13601361
gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)")
13611362
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
13621363
scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)")
@@ -1459,6 +1460,7 @@ func main() {
14591460
Target: *target,
14601461
StackSize: stackSize,
14611462
Opt: *opt,
1463+
LTO: *lto,
14621464
GC: *gc,
14631465
PanicStrategy: *panicStrategy,
14641466
Scheduler: *scheduler,

main_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ func TestBuild(t *testing.T) {
105105
runTestWithConfig("print.go", t, opts, nil, nil)
106106
})
107107

108+
t.Run("lto=thin", func(t *testing.T) {
109+
t.Parallel()
110+
opts := optionsFromTarget("", sema)
111+
opts.LTO = "thin"
112+
runTestWithConfig("init.go", t, opts, nil, nil)
113+
})
114+
108115
t.Run("ldflags", func(t *testing.T) {
109116
t.Parallel()
110117
opts := optionsFromTarget("", sema)

0 commit comments

Comments
 (0)