From 2de0635140a69233547a071ac464fff92a9746a2 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sun, 26 Feb 2023 22:18:45 +0100 Subject: [PATCH] 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: https://github.com/tinygo-org/tinygo/issues/2870 Once interface type asserts and method calls are converted to a vtable-like implementation, most programs should just work on linux/darwin/windows. --- builder/build.go | 57 ++++++++++++++++++++++++++++++++++-------- compileopts/config.go | 8 ++++++ compileopts/options.go | 8 ++++++ compiler/alias.go | 4 ++- compiler/compiler.go | 20 +++++++++++---- compiler/interface.go | 25 ++++++++++-------- compiler/interrupt.go | 4 ++- main.go | 2 ++ main_test.go | 7 ++++++ 9 files changed, 108 insertions(+), 27 deletions(-) diff --git a/builder/build.go b/builder/build.go index 034e335c5..4706fe8a6 100644 --- a/builder/build.go +++ b/builder/build.go @@ -176,6 +176,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe DefaultStackSize: config.StackSize(), NeedsStackObjects: config.NeedsStackObjects(), Debug: true, + LTO: config.LTO() != "legacy", } // Load the target machine, which is the LLVM object that contains all @@ -452,18 +453,24 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if err != nil { return err } - if runtime.GOOS == "windows" { - // Work around a problem on Windows. - // For some reason, WriteBitcodeToFile causes TinyGo to - // exit with the following message: - // LLVM ERROR: IO failure on output stream: Bad file descriptor - buf := llvm.WriteBitcodeToMemoryBuffer(mod) + if compilerConfig.LTO { + buf := llvm.WriteThinLTOBitcodeToMemoryBuffer(mod) defer buf.Dispose() _, err = f.Write(buf.Bytes()) } else { - // Otherwise, write bitcode directly to the file (probably - // faster). - err = llvm.WriteBitcodeToFile(mod, f) + if runtime.GOOS == "windows" { + // Work around a problem on Windows. + // For some reason, WriteBitcodeToFile causes TinyGo to + // exit with the following message: + // LLVM ERROR: IO failure on output stream: Bad file descriptor + buf := llvm.WriteBitcodeToMemoryBuffer(mod) + defer buf.Dispose() + _, err = f.Write(buf.Bytes()) + } else { + // Otherwise, write bitcode directly to the file (probably + // faster). + err = llvm.WriteBitcodeToFile(mod, f) + } } if err != nil { // WriteBitcodeToFile doesn't produce a useful error on its @@ -609,8 +616,38 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe }, } + // Add job to create the runtime.initAll function in a new module. + initAllJob := &compileJob{ + description: "create runtime.initAll", + result: filepath.Join(tmpdir, "runtime-initAll.bc"), + run: func(job *compileJob) (err error) { + // Create module with runtime.initAll. + ctx := llvm.NewContext() + defer ctx.Dispose() + initAllMod := createInitAll(ctx, config, compilerConfig, lprogram.Sorted()) + defer initAllMod.Dispose() + + // Write module to bitcode file. + llvmBuf := llvm.WriteThinLTOBitcodeToMemoryBuffer(initAllMod) + defer llvmBuf.Dispose() + return os.WriteFile(job.result, llvmBuf.Bytes(), 0666) + }, + } + // Prepare link command. - linkerDependencies := []*compileJob{outputObjectFileJob} + var linkerDependencies []*compileJob + switch config.LTO() { + case "legacy": + // Link all Go bitcode files together into a single large module and + // then do a ThinLTO link with the resulting large module + extra C + // files (from CGo etc). + linkerDependencies = append(linkerDependencies, outputObjectFileJob) + case "thin": + // Do a real thin link, with each Go package in a separate translation + // unit. This is faster than merging them into one big LTO module. + linkerDependencies = append(linkerDependencies, packageJobs...) + linkerDependencies = append(linkerDependencies, initAllJob) + } result.Executable = filepath.Join(tmpdir, "main") if config.GOOS() == "windows" { result.Executable += ".exe" diff --git a/compileopts/config.go b/compileopts/config.go index bfb02f1b7..9f095f9ea 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -165,6 +165,14 @@ func (c *Config) OptLevels() (optLevel, sizeLevel int, inlinerThreshold uint) { } } +// LTO returns one of the possible LTO configurations: legacy or thin. +func (c *Config) LTO() string { + if c.Options.LTO != "" { + return c.Options.LTO + } + return "legacy" +} + // PanicStrategy returns the panic strategy selected for this target. Valid // values are "print" (print the panic value, then exit) or "trap" (issue a trap // instruction). diff --git a/compileopts/options.go b/compileopts/options.go index 9c4cb10ae..c5386d201 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -14,6 +14,7 @@ var ( validPrintSizeOptions = []string{"none", "short", "full"} validPanicStrategyOptions = []string{"print", "trap"} validOptOptions = []string{"none", "0", "1", "2", "s", "z"} + validLTOOptions = []string{"legacy", "thin"} ) // Options contains extra options to give to the compiler. These options are @@ -25,6 +26,7 @@ type Options struct { GOARM string // environment variable (only used with GOARCH=arm) Target string Opt string + LTO string GC string PanicStrategy string Scheduler string @@ -107,6 +109,12 @@ func (o *Options) Verify() error { } } + if o.LTO != "" { + if !isInArray(validLTOOptions, o.LTO) { + return fmt.Errorf("invalid -lto=%s: valid values are %s", o.LTO, strings.Join(validLTOOptions, ", ")) + } + } + return nil } diff --git a/compiler/alias.go b/compiler/alias.go index b16cbce86..f064ae692 100644 --- a/compiler/alias.go +++ b/compiler/alias.go @@ -34,7 +34,9 @@ var stdlibAliases = map[string]string{ // createAlias implements the function (in the builder) as a call to the alias // function. func (b *builder) createAlias(alias llvm.Value) { - b.llvmFn.SetVisibility(llvm.HiddenVisibility) + if !b.LTO { + b.llvmFn.SetVisibility(llvm.HiddenVisibility) + } b.llvmFn.SetUnnamedAddr(true) if b.Debug { diff --git a/compiler/compiler.go b/compiler/compiler.go index 8b540d351..8f654eb36 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -55,6 +55,7 @@ type Config struct { DefaultStackSize uint64 NeedsStackObjects bool Debug bool // Whether to emit debug information in the LLVM module. + LTO bool // non-legacy LTO (meaning: package bitcode is merged by the linker) } // compilerContext contains function-independent data that should still be @@ -897,7 +898,9 @@ func (c *compilerContext) createPackage(irbuilder llvm.Builder, pkg *ssa.Package c.createEmbedGlobal(member, global, files) } else if !info.extern { global.SetInitializer(llvm.ConstNull(global.GlobalValueType())) - global.SetVisibility(llvm.HiddenVisibility) + if !c.LTO { + global.SetVisibility(llvm.HiddenVisibility) + } if info.section != "" { global.SetSection(info.section) } @@ -944,7 +947,9 @@ func (c *compilerContext) createEmbedGlobal(member *ssa.Global, global llvm.Valu } strObj := c.getEmbedFileString(files[0]) global.SetInitializer(strObj) - global.SetVisibility(llvm.HiddenVisibility) + if !c.LTO { + global.SetVisibility(llvm.HiddenVisibility) + } case *types.Slice: if typ.Elem().Underlying().(*types.Basic).Kind() != types.Byte { @@ -968,7 +973,9 @@ func (c *compilerContext) createEmbedGlobal(member *ssa.Global, global llvm.Valu sliceLen := llvm.ConstInt(c.uintptrType, file.Size, false) sliceObj := c.ctx.ConstStruct([]llvm.Value{slicePtr, sliceLen, sliceLen}, false) global.SetInitializer(sliceObj) - global.SetVisibility(llvm.HiddenVisibility) + if !c.LTO { + global.SetVisibility(llvm.HiddenVisibility) + } case *types.Struct: // Assume this is an embed.FS struct: @@ -1051,7 +1058,9 @@ func (c *compilerContext) createEmbedGlobal(member *ssa.Global, global llvm.Valu globalInitializer := llvm.ConstNull(c.getLLVMType(member.Type().(*types.Pointer).Elem())) globalInitializer = c.builder.CreateInsertValue(globalInitializer, sliceGlobal, 0, "") global.SetInitializer(globalInitializer) - global.SetVisibility(llvm.HiddenVisibility) + if !c.LTO { + global.SetVisibility(llvm.HiddenVisibility) + } global.SetAlignment(c.targetData.ABITypeAlignment(globalInitializer.Type())) } } @@ -1098,7 +1107,8 @@ func (b *builder) createFunctionStart(intrinsic bool) { // assertion error in llvm-project/llvm/include/llvm/IR/GlobalValue.h:236 // is thrown. if b.llvmFn.Linkage() != llvm.InternalLinkage && - b.llvmFn.Linkage() != llvm.PrivateLinkage { + b.llvmFn.Linkage() != llvm.PrivateLinkage && + !b.LTO { b.llvmFn.SetVisibility(llvm.HiddenVisibility) } b.llvmFn.SetUnnamedAddr(true) diff --git a/compiler/interface.go b/compiler/interface.go index a359f33a4..2f2a1c9d2 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -490,17 +490,22 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value { commaOk = b.CreateCall(fn.GlobalValueType(), fn, []llvm.Value{actualTypeNum}, "") } else { - globalName := "reflect/types.typeid:" + getTypeCodeName(expr.AssertedType) - assertedTypeCodeGlobal := b.mod.NamedGlobal(globalName) - if assertedTypeCodeGlobal.IsNil() { - // Create a new typecode global. - assertedTypeCodeGlobal = llvm.AddGlobal(b.mod, b.ctx.Int8Type(), globalName) - assertedTypeCodeGlobal.SetGlobalConstant(true) + if b.LTO { + assertedTypeCodeGlobal := b.getTypeCode(expr.AssertedType) + commaOk = b.CreateICmp(llvm.IntEQ, actualTypeNum, assertedTypeCodeGlobal, "commaok") + } else { + globalName := "reflect/types.typeid:" + getTypeCodeName(expr.AssertedType) + assertedTypeCodeGlobal := b.mod.NamedGlobal(globalName) + if assertedTypeCodeGlobal.IsNil() { + // Create a new typecode global. + assertedTypeCodeGlobal = llvm.AddGlobal(b.mod, b.ctx.Int8Type(), globalName) + assertedTypeCodeGlobal.SetGlobalConstant(true) + } + // Type assert on concrete type. + // Call runtime.typeAssert, which will be lowered to a simple icmp + // or const false in the interface lowering pass. + commaOk = b.createRuntimeCall("typeAssert", []llvm.Value{actualTypeNum, assertedTypeCodeGlobal}, "typecode") } - // Type assert on concrete type. - // Call runtime.typeAssert, which will be lowered to a simple icmp or - // const false in the interface lowering pass. - commaOk = b.createRuntimeCall("typeAssert", []llvm.Value{actualTypeNum, assertedTypeCodeGlobal}, "typecode") } // Add 2 new basic blocks (that should get optimized away): one for the diff --git a/compiler/interrupt.go b/compiler/interrupt.go index c1f7d69f2..6fb633710 100644 --- a/compiler/interrupt.go +++ b/compiler/interrupt.go @@ -45,7 +45,9 @@ func (b *builder) createInterruptGlobal(instr *ssa.CallCommon) (llvm.Value, erro globalLLVMType := b.getLLVMType(globalType) globalName := b.fn.Package().Pkg.Path() + "$interrupt" + strconv.FormatInt(id.Int64(), 10) global := llvm.AddGlobal(b.mod, globalLLVMType, globalName) - global.SetVisibility(llvm.HiddenVisibility) + if !b.LTO { + global.SetVisibility(llvm.HiddenVisibility) + } global.SetGlobalConstant(true) global.SetUnnamedAddr(true) initializer := llvm.ConstNull(globalLLVMType) diff --git a/main.go b/main.go index 3bfea182a..0d4841d20 100644 --- a/main.go +++ b/main.go @@ -1357,6 +1357,7 @@ func main() { command := os.Args[1] opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") + lto := flag.String("lto", "legacy", "LTO mode: legacy or thin") gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)") panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)") scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)") @@ -1459,6 +1460,7 @@ func main() { Target: *target, StackSize: stackSize, Opt: *opt, + LTO: *lto, GC: *gc, PanicStrategy: *panicStrategy, Scheduler: *scheduler, diff --git a/main_test.go b/main_test.go index 4de8fc09c..ed77b188a 100644 --- a/main_test.go +++ b/main_test.go @@ -105,6 +105,13 @@ func TestBuild(t *testing.T) { runTestWithConfig("print.go", t, opts, nil, nil) }) + t.Run("lto=thin", func(t *testing.T) { + t.Parallel() + opts := optionsFromTarget("", sema) + opts.LTO = "thin" + runTestWithConfig("init.go", t, opts, nil, nil) + }) + t.Run("ldflags", func(t *testing.T) { t.Parallel() opts := optionsFromTarget("", sema)