Skip to content

Add real ThinLTO mode #3489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 92 additions & 29 deletions builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,14 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
CodeModel: config.CodeModel(),
RelocationModel: config.RelocationModel(),
SizeLevel: sizeLevel,
TinyGoVersion: goenv.Version,

Scheduler: config.Scheduler(),
AutomaticStackSize: config.AutomaticStackSize(),
DefaultStackSize: config.StackSize(),
NeedsStackObjects: config.NeedsStackObjects(),
Debug: true,
LTO: config.LTO() != "legacy",
}

// Load the target machine, which is the LLVM object that contains all
Expand Down Expand Up @@ -305,7 +307,6 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
actionID := packageAction{
ImportPath: pkg.ImportPath,
CompilerBuildID: string(compilerBuildID),
TinyGoVersion: goenv.Version,
LLVMVersion: llvm.Version,
Config: compilerConfig,
CFlags: pkg.CFlags,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -511,24 +518,11 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe

// Create runtime.initAll function that calls the runtime
// initializer of each package.
llvmInitFn := mod.NamedFunction("runtime.initAll")
llvmInitFn.SetLinkage(llvm.InternalLinkage)
llvmInitFn.SetUnnamedAddr(true)
transform.AddStandardAttributes(llvmInitFn, config)
llvmInitFn.Param(0).SetName("context")
block := mod.Context().AddBasicBlock(llvmInitFn, "entry")
irbuilder := mod.Context().NewBuilder()
defer irbuilder.Dispose()
irbuilder.SetInsertPointAtEnd(block)
i8ptrType := llvm.PointerType(mod.Context().Int8Type(), 0)
for _, pkg := range lprogram.Sorted() {
pkgInit := mod.NamedFunction(pkg.Pkg.Path() + ".init")
if pkgInit.IsNil() {
panic("init not found for " + pkg.Pkg.Path())
}
irbuilder.CreateCall(pkgInit.GlobalValueType(), pkgInit, []llvm.Value{llvm.Undef(i8ptrType)}, "")
initAllModule := createInitAll(ctx, config, compilerConfig, lprogram.Sorted())
err := llvm.LinkModules(mod, initAllModule)
if err != nil {
return fmt.Errorf("failed to link module: %w", err)
}
irbuilder.CreateRetVoid()

// After linking, functions should (as far as possible) be set to
// private linkage or internal linkage. The compiler package marks
Expand Down Expand Up @@ -561,7 +555,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe

// Run all optimization passes, which are much more effective now
// that the optimizer can see the whole program at once.
err := optimizeProgram(mod, config)
err = optimizeProgram(mod, config)
if err != nil {
return err
}
Expand Down Expand Up @@ -622,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"
Expand Down Expand Up @@ -1032,6 +1056,45 @@ func createEmbedObjectFile(data, hexSum, sourceFile, sourceDir, tmpdir string, c
return outfile.Name(), outfile.Close()
}

// Create a module with the runtime.initAll function that calls all package
// initializers in initialization order.
func createInitAll(ctx llvm.Context, config *compileopts.Config, compilerConfig *compiler.Config, pkgs []*loader.Package) llvm.Module {
// Create LLVM module.
mod := ctx.NewModule("runtime-initAll")

// Add datalayout string.
machine, err := compiler.NewTargetMachine(compilerConfig)
if err != nil {
panic(err) // extremely unlikely to fail here (NewTargetMachine is called many times before)
}
defer machine.Dispose()
targetData := machine.CreateTargetData()
defer targetData.Dispose()
mod.SetTarget(config.Triple())
mod.SetDataLayout(targetData.String())

// Create empty runtime.initAll function.
i8ptrType := llvm.PointerType(mod.Context().Int8Type(), 0)
fnType := llvm.FunctionType(ctx.VoidType(), []llvm.Type{i8ptrType}, false)
fn := llvm.AddFunction(mod, "runtime.initAll", fnType)
fn.SetUnnamedAddr(true)
transform.AddStandardAttributes(fn, config)

// Add calls to package initializers.
fn.Param(0).SetName("context")
block := mod.Context().AddBasicBlock(fn, "entry")
irbuilder := mod.Context().NewBuilder()
defer irbuilder.Dispose()
irbuilder.SetInsertPointAtEnd(block)
for _, pkg := range pkgs {
pkgInit := llvm.AddFunction(mod, pkg.Pkg.Path()+".init", fnType)
irbuilder.CreateCall(fnType, pkgInit, []llvm.Value{llvm.Undef(i8ptrType)}, "")
}
irbuilder.CreateRetVoid()

return mod
}

// optimizeProgram runs a series of optimizations and transformations that are
// needed to convert a program to its final form. Some transformations are not
// optional and must be run as the compiler expects them to run.
Expand Down
8 changes: 8 additions & 0 deletions compileopts/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 8 additions & 0 deletions compileopts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 3 additions & 1 deletion compiler/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
29 changes: 24 additions & 5 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ type Config struct {
CodeModel string
RelocationModel string
SizeLevel int
TinyGoVersion string // for llvm.ident

// Various compiler options that determine how code is generated.
Scheduler string
AutomaticStackSize bool
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
Expand Down Expand Up @@ -321,6 +323,14 @@ func CompilePackage(moduleName string, pkg *loader.Package, ssaPkg *ssa.Package,
llvm.ConstInt(c.ctx.Int32Type(), 4, false).ConstantAsMetadata(),
}),
)
if c.TinyGoVersion != "" {
// It is necessary to set llvm.ident, otherwise debugging on MacOS
// won't work.
c.mod.AddNamedMetadataOperand("llvm.ident",
c.ctx.MDNode(([]llvm.Metadata{
c.ctx.MDString("TinyGo version " + c.TinyGoVersion),
})))
}
c.dibuilder.Finalize()
c.dibuilder.Destroy()
}
Expand Down Expand Up @@ -888,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)
}
Expand Down Expand Up @@ -935,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 {
Expand All @@ -959,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:
Expand Down Expand Up @@ -1042,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()))
}
}
Expand Down Expand Up @@ -1089,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)
Expand Down
25 changes: 15 additions & 10 deletions compiler/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion compiler/interrupt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down Expand Up @@ -1459,6 +1460,7 @@ func main() {
Target: *target,
StackSize: stackSize,
Opt: *opt,
LTO: *lto,
GC: *gc,
PanicStrategy: *panicStrategy,
Scheduler: *scheduler,
Expand Down
7 changes: 7 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down