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.
This commit is contained in:
Ayke van Laethem
2023-02-26 22:18:45 +01:00
parent 9b91cbb841
commit 2de0635140
9 changed files with 108 additions and 27 deletions
+38 -1
View File
@@ -176,6 +176,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
DefaultStackSize: config.StackSize(), DefaultStackSize: config.StackSize(),
NeedsStackObjects: config.NeedsStackObjects(), NeedsStackObjects: config.NeedsStackObjects(),
Debug: true, Debug: true,
LTO: config.LTO() != "legacy",
} }
// Load the target machine, which is the LLVM object that contains all // Load the target machine, which is the LLVM object that contains all
@@ -452,6 +453,11 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
if err != nil { if err != nil {
return err return err
} }
if compilerConfig.LTO {
buf := llvm.WriteThinLTOBitcodeToMemoryBuffer(mod)
defer buf.Dispose()
_, err = f.Write(buf.Bytes())
} else {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// Work around a problem on Windows. // Work around a problem on Windows.
// For some reason, WriteBitcodeToFile causes TinyGo to // For some reason, WriteBitcodeToFile causes TinyGo to
@@ -465,6 +471,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
// faster). // faster).
err = llvm.WriteBitcodeToFile(mod, f) err = llvm.WriteBitcodeToFile(mod, f)
} }
}
if err != nil { if err != nil {
// WriteBitcodeToFile doesn't produce a useful error on its // WriteBitcodeToFile doesn't produce a useful error on its
// own, so create a somewhat useful error message here. // own, so create a somewhat useful error message here.
@@ -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. // 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") result.Executable = filepath.Join(tmpdir, "main")
if config.GOOS() == "windows" { if config.GOOS() == "windows" {
result.Executable += ".exe" result.Executable += ".exe"
+8
View File
@@ -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 // PanicStrategy returns the panic strategy selected for this target. Valid
// values are "print" (print the panic value, then exit) or "trap" (issue a trap // values are "print" (print the panic value, then exit) or "trap" (issue a trap
// instruction). // instruction).
+8
View File
@@ -14,6 +14,7 @@ var (
validPrintSizeOptions = []string{"none", "short", "full"} validPrintSizeOptions = []string{"none", "short", "full"}
validPanicStrategyOptions = []string{"print", "trap"} validPanicStrategyOptions = []string{"print", "trap"}
validOptOptions = []string{"none", "0", "1", "2", "s", "z"} validOptOptions = []string{"none", "0", "1", "2", "s", "z"}
validLTOOptions = []string{"legacy", "thin"}
) )
// Options contains extra options to give to the compiler. These options are // 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) GOARM string // environment variable (only used with GOARCH=arm)
Target string Target string
Opt string Opt string
LTO string
GC string GC string
PanicStrategy string PanicStrategy string
Scheduler 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 return nil
} }
+2
View File
@@ -34,7 +34,9 @@ var stdlibAliases = map[string]string{
// createAlias implements the function (in the builder) as a call to the alias // createAlias implements the function (in the builder) as a call to the alias
// function. // function.
func (b *builder) createAlias(alias llvm.Value) { func (b *builder) createAlias(alias llvm.Value) {
if !b.LTO {
b.llvmFn.SetVisibility(llvm.HiddenVisibility) b.llvmFn.SetVisibility(llvm.HiddenVisibility)
}
b.llvmFn.SetUnnamedAddr(true) b.llvmFn.SetUnnamedAddr(true)
if b.Debug { if b.Debug {
+11 -1
View File
@@ -55,6 +55,7 @@ type Config struct {
DefaultStackSize uint64 DefaultStackSize uint64
NeedsStackObjects bool NeedsStackObjects bool
Debug bool // Whether to emit debug information in the LLVM module. 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 // 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) c.createEmbedGlobal(member, global, files)
} else if !info.extern { } else if !info.extern {
global.SetInitializer(llvm.ConstNull(global.GlobalValueType())) global.SetInitializer(llvm.ConstNull(global.GlobalValueType()))
if !c.LTO {
global.SetVisibility(llvm.HiddenVisibility) global.SetVisibility(llvm.HiddenVisibility)
}
if info.section != "" { if info.section != "" {
global.SetSection(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]) strObj := c.getEmbedFileString(files[0])
global.SetInitializer(strObj) global.SetInitializer(strObj)
if !c.LTO {
global.SetVisibility(llvm.HiddenVisibility) global.SetVisibility(llvm.HiddenVisibility)
}
case *types.Slice: case *types.Slice:
if typ.Elem().Underlying().(*types.Basic).Kind() != types.Byte { 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) sliceLen := llvm.ConstInt(c.uintptrType, file.Size, false)
sliceObj := c.ctx.ConstStruct([]llvm.Value{slicePtr, sliceLen, sliceLen}, false) sliceObj := c.ctx.ConstStruct([]llvm.Value{slicePtr, sliceLen, sliceLen}, false)
global.SetInitializer(sliceObj) global.SetInitializer(sliceObj)
if !c.LTO {
global.SetVisibility(llvm.HiddenVisibility) global.SetVisibility(llvm.HiddenVisibility)
}
case *types.Struct: case *types.Struct:
// Assume this is an embed.FS 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 := llvm.ConstNull(c.getLLVMType(member.Type().(*types.Pointer).Elem()))
globalInitializer = c.builder.CreateInsertValue(globalInitializer, sliceGlobal, 0, "") globalInitializer = c.builder.CreateInsertValue(globalInitializer, sliceGlobal, 0, "")
global.SetInitializer(globalInitializer) global.SetInitializer(globalInitializer)
if !c.LTO {
global.SetVisibility(llvm.HiddenVisibility) global.SetVisibility(llvm.HiddenVisibility)
}
global.SetAlignment(c.targetData.ABITypeAlignment(globalInitializer.Type())) 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 // assertion error in llvm-project/llvm/include/llvm/IR/GlobalValue.h:236
// is thrown. // is thrown.
if b.llvmFn.Linkage() != llvm.InternalLinkage && 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.SetVisibility(llvm.HiddenVisibility)
} }
b.llvmFn.SetUnnamedAddr(true) b.llvmFn.SetUnnamedAddr(true)
+7 -2
View File
@@ -489,6 +489,10 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value {
fn := b.getInterfaceImplementsFunc(expr.AssertedType) fn := b.getInterfaceImplementsFunc(expr.AssertedType)
commaOk = b.CreateCall(fn.GlobalValueType(), fn, []llvm.Value{actualTypeNum}, "") commaOk = b.CreateCall(fn.GlobalValueType(), fn, []llvm.Value{actualTypeNum}, "")
} else {
if b.LTO {
assertedTypeCodeGlobal := b.getTypeCode(expr.AssertedType)
commaOk = b.CreateICmp(llvm.IntEQ, actualTypeNum, assertedTypeCodeGlobal, "commaok")
} else { } else {
globalName := "reflect/types.typeid:" + getTypeCodeName(expr.AssertedType) globalName := "reflect/types.typeid:" + getTypeCodeName(expr.AssertedType)
assertedTypeCodeGlobal := b.mod.NamedGlobal(globalName) assertedTypeCodeGlobal := b.mod.NamedGlobal(globalName)
@@ -498,10 +502,11 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value {
assertedTypeCodeGlobal.SetGlobalConstant(true) assertedTypeCodeGlobal.SetGlobalConstant(true)
} }
// Type assert on concrete type. // Type assert on concrete type.
// Call runtime.typeAssert, which will be lowered to a simple icmp or // Call runtime.typeAssert, which will be lowered to a simple icmp
// const false in the interface lowering pass. // or const false in the interface lowering pass.
commaOk = b.createRuntimeCall("typeAssert", []llvm.Value{actualTypeNum, assertedTypeCodeGlobal}, "typecode") commaOk = b.createRuntimeCall("typeAssert", []llvm.Value{actualTypeNum, assertedTypeCodeGlobal}, "typecode")
} }
}
// Add 2 new basic blocks (that should get optimized away): one for the // Add 2 new basic blocks (that should get optimized away): one for the
// 'ok' case and one for all instructions following this type assert. // 'ok' case and one for all instructions following this type assert.
+2
View File
@@ -45,7 +45,9 @@ func (b *builder) createInterruptGlobal(instr *ssa.CallCommon) (llvm.Value, erro
globalLLVMType := b.getLLVMType(globalType) globalLLVMType := b.getLLVMType(globalType)
globalName := b.fn.Package().Pkg.Path() + "$interrupt" + strconv.FormatInt(id.Int64(), 10) globalName := b.fn.Package().Pkg.Path() + "$interrupt" + strconv.FormatInt(id.Int64(), 10)
global := llvm.AddGlobal(b.mod, globalLLVMType, globalName) global := llvm.AddGlobal(b.mod, globalLLVMType, globalName)
if !b.LTO {
global.SetVisibility(llvm.HiddenVisibility) global.SetVisibility(llvm.HiddenVisibility)
}
global.SetGlobalConstant(true) global.SetGlobalConstant(true)
global.SetUnnamedAddr(true) global.SetUnnamedAddr(true)
initializer := llvm.ConstNull(globalLLVMType) initializer := llvm.ConstNull(globalLLVMType)
+2
View File
@@ -1357,6 +1357,7 @@ func main() {
command := os.Args[1] command := os.Args[1]
opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") 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)") gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)")
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)") panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)") scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)")
@@ -1459,6 +1460,7 @@ func main() {
Target: *target, Target: *target,
StackSize: stackSize, StackSize: stackSize,
Opt: *opt, Opt: *opt,
LTO: *lto,
GC: *gc, GC: *gc,
PanicStrategy: *panicStrategy, PanicStrategy: *panicStrategy,
Scheduler: *scheduler, Scheduler: *scheduler,
+7
View File
@@ -105,6 +105,13 @@ func TestBuild(t *testing.T) {
runTestWithConfig("print.go", t, opts, nil, nil) 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.Run("ldflags", func(t *testing.T) {
t.Parallel() t.Parallel()
opts := optionsFromTarget("", sema) opts := optionsFromTarget("", sema)