diff --git a/builder/build.go b/builder/build.go index e7029afd..3ce7f644 100644 --- a/builder/build.go +++ b/builder/build.go @@ -72,16 +72,16 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri // exactly. errs = nil switch config.Options.Opt { - case "none:", "0": - errs = c.Optimize(0, 0, 0) // -O0 + case "none", "0": + errs = transform.Optimize(c.Module(), config, 0, 0, 0) // -O0 case "1": - errs = c.Optimize(1, 0, 0) // -O1 + errs = transform.Optimize(c.Module(), config, 1, 0, 0) // -O1 case "2": - errs = c.Optimize(2, 0, 225) // -O2 + errs = transform.Optimize(c.Module(), config, 2, 0, 225) // -O2 case "s": - errs = c.Optimize(2, 1, 225) // -Os + errs = transform.Optimize(c.Module(), config, 2, 1, 225) // -Os case "z": - errs = c.Optimize(2, 2, 5) // -Oz, default + errs = transform.Optimize(c.Module(), config, 2, 2, 5) // -Oz, default default: errs = []error{errors.New("unknown optimization level: -opt=" + config.Options.Opt)} } diff --git a/compiler/compiler.go b/compiler/compiler.go index 609d57f7..63b1dc92 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -34,30 +34,6 @@ func init() { // The TinyGo import path. const tinygoPath = "github.com/tinygo-org/tinygo" -// functionsUsedInTransform is a list of function symbols that may be used -// during TinyGo optimization passes so they have to be marked as external -// linkage until all TinyGo passes have finished. -var functionsUsedInTransforms = []string{ - "runtime.alloc", - "runtime.free", - "runtime.nilPanic", -} - -var taskFunctionsUsedInTransforms = []string{} - -var coroFunctionsUsedInTransforms = []string{ - "internal/task.start", - "internal/task.Pause", - "internal/task.fake", - "internal/task.Current", - "internal/task.createTask", - "(*internal/task.Task).setState", - "(*internal/task.Task).returnTo", - "(*internal/task.Task).returnCurrent", - "(*internal/task.Task).setReturnPtr", - "(*internal/task.Task).getReturnPtr", -} - type Compiler struct { *compileopts.Config mod llvm.Module @@ -156,21 +132,6 @@ func (c *Compiler) Module() llvm.Module { return c.mod } -// getFunctionsUsedInTransforms gets a list of all special functions that should be preserved during transforms and optimization. -func (c *Compiler) getFunctionsUsedInTransforms() []string { - fnused := functionsUsedInTransforms - switch c.Scheduler() { - case "none": - case "coroutines": - fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...) - case "tasks": - fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...) - default: - panic(fmt.Errorf("invalid scheduler %q", c.Scheduler())) - } - return fnused -} - // Compile the given package path or .go file path. Return an error when this // fails (in any stage). func (c *Compiler) Compile(mainPath string) []error { @@ -338,14 +299,8 @@ func (c *Compiler) Compile(mainPath string) []error { realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main") realMain.SetLinkage(llvm.ExternalLinkage) // keep alive until goroutine lowering - // Make sure these functions are kept in tact during TinyGo transformation passes. - for _, name := range c.getFunctionsUsedInTransforms() { - fn := c.mod.NamedFunction(name) - if fn.IsNil() { - panic(fmt.Errorf("missing core function %q", name)) - } - fn.SetLinkage(llvm.ExternalLinkage) - } + // Replace callMain placeholder with actual main function. + c.mod.NamedFunction("runtime.callMain").ReplaceAllUsesWith(realMain) // Load some attributes getAttr := func(attrName string) llvm.Attribute { diff --git a/compiler/optimizer.go b/compiler/optimizer.go deleted file mode 100644 index 4b08eb44..00000000 --- a/compiler/optimizer.go +++ /dev/null @@ -1,185 +0,0 @@ -package compiler - -import ( - "errors" - - "github.com/tinygo-org/tinygo/compileopts" - "github.com/tinygo-org/tinygo/compiler/ircheck" - "github.com/tinygo-org/tinygo/transform" - "tinygo.org/x/go-llvm" -) - -// Run the LLVM optimizer over the module. -// The inliner can be disabled (if necessary) by passing 0 to the inlinerThreshold. -func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) []error { - builder := llvm.NewPassManagerBuilder() - defer builder.Dispose() - builder.SetOptLevel(optLevel) - builder.SetSizeLevel(sizeLevel) - if inlinerThreshold != 0 { - builder.UseInlinerWithThreshold(inlinerThreshold) - } - builder.AddCoroutinePassesToExtensionPoints() - - if c.PanicStrategy() == "trap" { - transform.ReplacePanicsWithTrap(c.mod) // -panic=trap - } - - // run a check of all of our code - if c.VerifyIR() { - errs := ircheck.Module(c.mod) - if errs != nil { - return errs - } - } - - // Replace callMain placeholder with actual main function. - c.mod.NamedFunction("runtime.callMain").ReplaceAllUsesWith(c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main")) - - // Run function passes for each function. - funcPasses := llvm.NewFunctionPassManagerForModule(c.mod) - defer funcPasses.Dispose() - builder.PopulateFunc(funcPasses) - funcPasses.InitializeFunc() - for fn := c.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - funcPasses.RunFunc(fn) - } - funcPasses.FinalizeFunc() - - if optLevel > 0 { - // Run some preparatory passes for the Go optimizer. - goPasses := llvm.NewPassManager() - defer goPasses.Dispose() - goPasses.AddGlobalDCEPass() - goPasses.AddGlobalOptimizerPass() - goPasses.AddConstantPropagationPass() - goPasses.AddAggressiveDCEPass() - goPasses.AddFunctionAttrsPass() - goPasses.Run(c.mod) - - // Run Go-specific optimization passes. - transform.OptimizeMaps(c.mod) - transform.OptimizeStringToBytes(c.mod) - transform.OptimizeAllocs(c.mod) - transform.LowerInterfaces(c.mod) - - errs := transform.LowerInterrupts(c.mod) - if len(errs) > 0 { - return errs - } - - if c.FuncImplementation() == compileopts.FuncValueSwitch { - transform.LowerFuncValues(c.mod) - } - - // After interfaces are lowered, there are many more opportunities for - // interprocedural optimizations. To get them to work, function - // attributes have to be updated first. - goPasses.Run(c.mod) - - // Run TinyGo-specific interprocedural optimizations. - transform.OptimizeAllocs(c.mod) - transform.OptimizeStringToBytes(c.mod) - - // Lower runtime.isnil calls to regular nil comparisons. - isnil := c.mod.NamedFunction("runtime.isnil") - if !isnil.IsNil() { - for _, use := range getUses(isnil) { - c.builder.SetInsertPointBefore(use) - ptr := use.Operand(0) - if !ptr.IsABitCastInst().IsNil() { - ptr = ptr.Operand(0) - } - nilptr := llvm.ConstPointerNull(ptr.Type()) - icmp := c.builder.CreateICmp(llvm.IntEQ, ptr, nilptr, "") - use.ReplaceAllUsesWith(icmp) - use.EraseFromParentAsInstruction() - } - } - - } else { - // Must be run at any optimization level. - transform.LowerInterfaces(c.mod) - if c.FuncImplementation() == compileopts.FuncValueSwitch { - transform.LowerFuncValues(c.mod) - } - errs := transform.LowerInterrupts(c.mod) - if len(errs) > 0 { - return errs - } - } - - // Lower async implementations. - switch c.Scheduler() { - case "coroutines": - // Lower async as coroutines. - err := transform.LowerCoroutines(c.mod, c.NeedsStackObjects()) - if err != nil { - return []error{err} - } - case "tasks": - // No transformations necessary. - case "none": - // Check for any goroutine starts. - if start := c.mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 { - errs := []error{} - for _, call := range getUses(start) { - errs = append(errs, errorAt(call, "attempted to start a goroutine without a scheduler")) - } - return errs - } - default: - return []error{errors.New("invalid scheduler")} - } - - if c.VerifyIR() { - if errs := ircheck.Module(c.mod); errs != nil { - return errs - } - } - if err := c.Verify(); err != nil { - return []error{errors.New("optimizations caused a verification failure")} - } - - if sizeLevel >= 2 { - // Set the "optsize" attribute to make slightly smaller binaries at the - // cost of some performance. - kind := llvm.AttributeKindID("optsize") - attr := c.ctx.CreateEnumAttribute(kind, 0) - for fn := c.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - fn.AddFunctionAttr(attr) - } - } - - // After TinyGo-specific transforms have finished, undo exporting these functions. - for _, name := range c.getFunctionsUsedInTransforms() { - fn := c.mod.NamedFunction(name) - if fn.IsNil() { - continue - } - fn.SetLinkage(llvm.InternalLinkage) - } - - // Run function passes again, because without it, llvm.coro.size.i32() - // doesn't get lowered. - for fn := c.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - funcPasses.RunFunc(fn) - } - funcPasses.FinalizeFunc() - - // Run module passes. - modPasses := llvm.NewPassManager() - defer modPasses.Dispose() - builder.Populate(modPasses) - modPasses.Run(c.mod) - - hasGCPass := transform.AddGlobalsBitmap(c.mod) - hasGCPass = transform.MakeGCStackSlots(c.mod) || hasGCPass - if hasGCPass { - if err := c.Verify(); err != nil { - return []error{errors.New("GC pass caused a verification failure")} - } - } - - return nil -} diff --git a/transform/coroutines.go b/transform/coroutines.go index 5df2d746..d1609b65 100644 --- a/transform/coroutines.go +++ b/transform/coroutines.go @@ -5,8 +5,9 @@ package transform import ( "errors" - "github.com/tinygo-org/tinygo/compiler/llvmutil" "strconv" + + "github.com/tinygo-org/tinygo/compiler/llvmutil" "tinygo.org/x/go-llvm" ) diff --git a/transform/optimizer.go b/transform/optimizer.go new file mode 100644 index 00000000..71bf4d61 --- /dev/null +++ b/transform/optimizer.go @@ -0,0 +1,242 @@ +package transform + +import ( + "errors" + "fmt" + + "github.com/tinygo-org/tinygo/compileopts" + "github.com/tinygo-org/tinygo/compiler/ircheck" + "tinygo.org/x/go-llvm" +) + +// Optimize runs a number of optimization and transformation passes over the +// given module. Some passes are specific to TinyGo, others are generic LLVM +// passes. You can set a preferred performance (0-3) and size (0-2) level and +// control the limits of the inliner (higher numbers mean more inlining, set it +// to 0 to disable entirely). +// +// Please note that some optimizations are not optional, thus Optimize must +// alwasy be run before emitting machine code. Set all controls (optLevel, +// sizeLevel, inlinerThreshold) to 0 to reduce the number of optimizations to a +// minimum. +func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel int, inlinerThreshold uint) []error { + builder := llvm.NewPassManagerBuilder() + defer builder.Dispose() + builder.SetOptLevel(optLevel) + builder.SetSizeLevel(sizeLevel) + if inlinerThreshold != 0 { + builder.UseInlinerWithThreshold(inlinerThreshold) + } + builder.AddCoroutinePassesToExtensionPoints() + + // Make sure these functions are kept in tact during TinyGo transformation passes. + for _, name := range getFunctionsUsedInTransforms(config) { + fn := mod.NamedFunction(name) + if fn.IsNil() { + panic(fmt.Errorf("missing core function %q", name)) + } + fn.SetLinkage(llvm.ExternalLinkage) + } + + if config.PanicStrategy() == "trap" { + ReplacePanicsWithTrap(mod) // -panic=trap + } + + // run a check of all of our code + if config.VerifyIR() { + errs := ircheck.Module(mod) + if errs != nil { + return errs + } + } + + // Run function passes for each function. + funcPasses := llvm.NewFunctionPassManagerForModule(mod) + defer funcPasses.Dispose() + builder.PopulateFunc(funcPasses) + funcPasses.InitializeFunc() + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + funcPasses.RunFunc(fn) + } + funcPasses.FinalizeFunc() + + if optLevel > 0 { + // Run some preparatory passes for the Go optimizer. + goPasses := llvm.NewPassManager() + defer goPasses.Dispose() + goPasses.AddGlobalDCEPass() + goPasses.AddGlobalOptimizerPass() + goPasses.AddConstantPropagationPass() + goPasses.AddAggressiveDCEPass() + goPasses.AddFunctionAttrsPass() + goPasses.Run(mod) + + // Run Go-specific optimization passes. + OptimizeMaps(mod) + OptimizeStringToBytes(mod) + OptimizeAllocs(mod) + LowerInterfaces(mod) + + errs := LowerInterrupts(mod) + if len(errs) > 0 { + return errs + } + + if config.FuncImplementation() == compileopts.FuncValueSwitch { + LowerFuncValues(mod) + } + + // After interfaces are lowered, there are many more opportunities for + // interprocedural optimizations. To get them to work, function + // attributes have to be updated first. + goPasses.Run(mod) + + // Run TinyGo-specific interprocedural optimizations. + OptimizeAllocs(mod) + OptimizeStringToBytes(mod) + + // Lower runtime.isnil calls to regular nil comparisons. + isnil := mod.NamedFunction("runtime.isnil") + if !isnil.IsNil() { + builder := mod.Context().NewBuilder() + defer builder.Dispose() + for _, use := range getUses(isnil) { + builder.SetInsertPointBefore(use) + ptr := use.Operand(0) + if !ptr.IsABitCastInst().IsNil() { + ptr = ptr.Operand(0) + } + nilptr := llvm.ConstPointerNull(ptr.Type()) + icmp := builder.CreateICmp(llvm.IntEQ, ptr, nilptr, "") + use.ReplaceAllUsesWith(icmp) + use.EraseFromParentAsInstruction() + } + } + + } else { + // Must be run at any optimization level. + LowerInterfaces(mod) + if config.FuncImplementation() == compileopts.FuncValueSwitch { + LowerFuncValues(mod) + } + errs := LowerInterrupts(mod) + if len(errs) > 0 { + return errs + } + } + + // Lower async implementations. + switch config.Scheduler() { + case "coroutines": + // Lower async as coroutines. + err := LowerCoroutines(mod, config.NeedsStackObjects()) + if err != nil { + return []error{err} + } + case "tasks": + // No transformations necessary. + case "none": + // Check for any goroutine starts. + if start := mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 { + errs := []error{} + for _, call := range getUses(start) { + errs = append(errs, errorAt(call, "attempted to start a goroutine without a scheduler")) + } + return errs + } + default: + return []error{errors.New("invalid scheduler")} + } + + if config.VerifyIR() { + if errs := ircheck.Module(mod); errs != nil { + return errs + } + } + if err := llvm.VerifyModule(mod, llvm.PrintMessageAction); err != nil { + return []error{errors.New("optimizations caused a verification failure")} + } + + if sizeLevel >= 2 { + // Set the "optsize" attribute to make slightly smaller binaries at the + // cost of some performance. + kind := llvm.AttributeKindID("optsize") + attr := mod.Context().CreateEnumAttribute(kind, 0) + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + fn.AddFunctionAttr(attr) + } + } + + // After TinyGo-specific transforms have finished, undo exporting these functions. + for _, name := range getFunctionsUsedInTransforms(config) { + fn := mod.NamedFunction(name) + if fn.IsNil() { + continue + } + fn.SetLinkage(llvm.InternalLinkage) + } + + // Run function passes again, because without it, llvm.coro.size.i32() + // doesn't get lowered. + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + funcPasses.RunFunc(fn) + } + funcPasses.FinalizeFunc() + + // Run module passes. + modPasses := llvm.NewPassManager() + defer modPasses.Dispose() + builder.Populate(modPasses) + modPasses.Run(mod) + + hasGCPass := AddGlobalsBitmap(mod) + hasGCPass = MakeGCStackSlots(mod) || hasGCPass + if hasGCPass { + if err := llvm.VerifyModule(mod, llvm.PrintMessageAction); err != nil { + return []error{errors.New("GC pass caused a verification failure")} + } + } + + return nil +} + +// functionsUsedInTransform is a list of function symbols that may be used +// during TinyGo optimization passes so they have to be marked as external +// linkage until all TinyGo passes have finished. +var functionsUsedInTransforms = []string{ + "runtime.alloc", + "runtime.free", + "runtime.nilPanic", +} + +var taskFunctionsUsedInTransforms = []string{} + +// These functions need to be preserved in the IR until after the coroutines +// pass has run. +var coroFunctionsUsedInTransforms = []string{ + "internal/task.start", + "internal/task.Pause", + "internal/task.fake", + "internal/task.Current", + "internal/task.createTask", + "(*internal/task.Task).setState", + "(*internal/task.Task).returnTo", + "(*internal/task.Task).returnCurrent", + "(*internal/task.Task).setReturnPtr", + "(*internal/task.Task).getReturnPtr", +} + +// getFunctionsUsedInTransforms gets a list of all special functions that should be preserved during transforms and optimization. +func getFunctionsUsedInTransforms(config *compileopts.Config) []string { + fnused := functionsUsedInTransforms + switch config.Scheduler() { + case "none": + case "coroutines": + fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...) + case "tasks": + fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...) + default: + panic(fmt.Errorf("invalid scheduler %q", config.Scheduler())) + } + return fnused +}