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)