diff options
author | Ayke van Laethem <[email protected]> | 2021-04-04 17:50:47 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2021-04-08 11:40:59 +0200 |
commit | 49ec3eb58e3ae2fd373b1c65883dcd05c588357a (patch) | |
tree | 3fe8230927609f5a5d767a2d6b5bbcb782196b44 | |
parent | fa6c1b69cea188ab28a9c7871921c9181d532de0 (diff) | |
download | tinygo-49ec3eb58e3ae2fd373b1c65883dcd05c588357a.tar.gz tinygo-49ec3eb58e3ae2fd373b1c65883dcd05c588357a.zip |
builder: add optsize attribute while building the package
This simplifies future changes. While the move itself is very simple, it
required some other changes to a few transforms that create new
functions to add the optsize attribute manually. It also required
abstracting away the optimization level flags (based on the -opt flag)
so that it can easily be retrieved from the config object.
This commit does not impact binary size on baremetal and WebAssembly.
I've seen a few tests on linux/amd64 grow slightly in size, but I'm not
too worried about those.
-rw-r--r-- | builder/build.go | 30 | ||||
-rw-r--r-- | compileopts/config.go | 21 | ||||
-rw-r--r-- | compileopts/options.go | 7 | ||||
-rw-r--r-- | transform/interface-lowering.go | 10 | ||||
-rw-r--r-- | transform/interface-lowering_test.go | 2 | ||||
-rw-r--r-- | transform/interrupt.go | 5 | ||||
-rw-r--r-- | transform/interrupt_test.go | 2 | ||||
-rw-r--r-- | transform/optimizer.go | 18 | ||||
-rw-r--r-- | transform/wasm-abi.go | 4 |
9 files changed, 66 insertions, 33 deletions
diff --git a/builder/build.go b/builder/build.go index 1a9ad9c1b..73981e71e 100644 --- a/builder/build.go +++ b/builder/build.go @@ -60,6 +60,7 @@ type packageAction struct { CFlags []string FileHashes map[string]string // hash of every file that's part of the package Imports map[string]string // map from imported package to action ID hash + SizeLevel int // LLVM optimization for size level (0-2) } // Build performs a single package to executable Go build. It takes in a package @@ -127,6 +128,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil var packageJobs []*compileJob packageBitcodePaths := make(map[string]string) packageActionIDs := make(map[string]string) + _, sizeLevel, _ := config.OptLevels() for _, pkg := range lprogram.Sorted() { pkg := pkg // necessary to avoid a race condition @@ -141,6 +143,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil CFlags: pkg.CFlags, FileHashes: make(map[string]string, len(pkg.FileHashes)), Imports: make(map[string]string, len(pkg.Pkg.Imports())), + SizeLevel: sizeLevel, } for filePath, hash := range pkg.FileHashes { actionID.FileHashes[filePath] = hex.EncodeToString(hash) @@ -206,6 +209,16 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil return errors.New("verification error after interpreting " + pkgInit.Name()) } + 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) + } + } + // Serialize the LLVM module as a bitcode file. // Write to a temporary path that is renamed to the destination // file to avoid race conditions with other TinyGo invocatiosn @@ -617,21 +630,8 @@ func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { // Optimization levels here are roughly the same as Clang, but probably not // exactly. - var errs []error - switch config.Options.Opt { - case "none", "0": - errs = transform.Optimize(mod, config, 0, 0, 0) // -O0 - case "1": - errs = transform.Optimize(mod, config, 1, 0, 0) // -O1 - case "2": - errs = transform.Optimize(mod, config, 2, 0, 225) // -O2 - case "s": - errs = transform.Optimize(mod, config, 2, 1, 225) // -Os - case "z": - errs = transform.Optimize(mod, config, 2, 2, 5) // -Oz, default - default: - return errors.New("unknown optimization level: -opt=" + config.Options.Opt) - } + optLevel, sizeLevel, inlinerThreshold := config.OptLevels() + errs := transform.Optimize(mod, config, optLevel, sizeLevel, inlinerThreshold) if len(errs) > 0 { return newMultiError(errs) } diff --git a/compileopts/config.go b/compileopts/config.go index 50a8aeab6..58af7a8c5 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -118,6 +118,27 @@ func (c *Config) Scheduler() string { return "coroutines" } +// OptLevels returns the optimization level (0-2), size level (0-2), and inliner +// threshold as used in the LLVM optimization pipeline. +func (c *Config) OptLevels() (optLevel, sizeLevel int, inlinerThreshold uint) { + switch c.Options.Opt { + case "none", "0": + return 0, 0, 0 // -O0 + case "1": + return 1, 0, 0 // -O1 + case "2": + return 2, 0, 225 // -O2 + case "s": + return 2, 1, 225 // -Os + case "z": + return 2, 2, 5 // -Oz, default + default: + // This is not shown to the user: valid choices are already checked as + // part of Options.Verify(). It is here as a sanity check. + panic("unknown optimization level: -opt=" + c.Options.Opt) + } +} + // FuncImplementation picks an appropriate func value implementation for the // target. func (c *Config) FuncImplementation() string { diff --git a/compileopts/options.go b/compileopts/options.go index f5f5a222d..d1c7c6edc 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -10,6 +10,7 @@ var ( validSchedulerOptions = []string{"none", "tasks", "coroutines"} validPrintSizeOptions = []string{"none", "short", "full"} validPanicStrategyOptions = []string{"print", "trap"} + validOptOptions = []string{"none", "0", "1", "2", "s", "z"} ) // Options contains extra options to give to the compiler. These options are @@ -73,6 +74,12 @@ func (o *Options) Verify() error { } } + if o.Opt != "" { + if !isInArray(validOptOptions, o.Opt) { + return fmt.Errorf("invalid -opt=%s: valid values are %s", o.Opt, strings.Join(validOptOptions, ", ")) + } + } + return nil } diff --git a/transform/interface-lowering.go b/transform/interface-lowering.go index 56956a39a..e8b6deecf 100644 --- a/transform/interface-lowering.go +++ b/transform/interface-lowering.go @@ -133,6 +133,7 @@ func (itf *interfaceInfo) id() string { // should be seen as a regular function call (see LowerInterfaces). type lowerInterfacesPass struct { mod llvm.Module + sizeLevel int // LLVM optimization size level, 1 means -opt=s and 2 means -opt=z builder llvm.Builder ctx llvm.Context uintptrType llvm.Type @@ -145,9 +146,10 @@ type lowerInterfacesPass struct { // emitted by the compiler as higher-level intrinsics. They need some lowering // before LLVM can work on them. This is done so that a few cleanup passes can // run before assigning the final type codes. -func LowerInterfaces(mod llvm.Module) error { +func LowerInterfaces(mod llvm.Module, sizeLevel int) error { p := &lowerInterfacesPass{ mod: mod, + sizeLevel: sizeLevel, builder: mod.Context().NewBuilder(), ctx: mod.Context(), uintptrType: mod.Context().IntType(llvm.NewTargetData(mod.DataLayout()).PointerSize() * 8), @@ -594,6 +596,9 @@ func (p *lowerInterfacesPass) createInterfaceImplementsFunc(itf *interfaceInfo) fn := itf.assertFunc fn.SetLinkage(llvm.InternalLinkage) fn.SetUnnamedAddr(true) + if p.sizeLevel >= 2 { + fn.AddFunctionAttr(p.ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0)) + } // TODO: debug info @@ -653,6 +658,9 @@ func (p *lowerInterfacesPass) createInterfaceMethodFunc(itf *interfaceInfo, sign fn := itf.methodFuncs[signature] fn.SetLinkage(llvm.InternalLinkage) fn.SetUnnamedAddr(true) + if p.sizeLevel >= 2 { + fn.AddFunctionAttr(p.ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0)) + } // TODO: debug info diff --git a/transform/interface-lowering_test.go b/transform/interface-lowering_test.go index f3652cc90..e1c3c2dcc 100644 --- a/transform/interface-lowering_test.go +++ b/transform/interface-lowering_test.go @@ -9,7 +9,7 @@ import ( func TestInterfaceLowering(t *testing.T) { t.Parallel() testTransform(t, "testdata/interface", func(mod llvm.Module) { - err := LowerInterfaces(mod) + err := LowerInterfaces(mod, 0) if err != nil { t.Error(err) } diff --git a/transform/interrupt.go b/transform/interrupt.go index 1c3247d5e..d5d7d47e9 100644 --- a/transform/interrupt.go +++ b/transform/interrupt.go @@ -22,7 +22,7 @@ import ( // simply call the registered handlers. This might seem like it causes extra // overhead, but in fact inlining and const propagation will eliminate most if // not all of that. -func LowerInterrupts(mod llvm.Module) []error { +func LowerInterrupts(mod llvm.Module, sizeLevel int) []error { var errs []error // Discover interrupts. The runtime/interrupt.Register call is a compiler @@ -182,6 +182,9 @@ func LowerInterrupts(mod llvm.Module) []error { // Create the wrapper function which is the actual interrupt handler // that is inserted in the interrupt vector. fn.SetUnnamedAddr(true) + if sizeLevel >= 2 { + fn.AddFunctionAttr(ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0)) + } fn.SetSection(".text." + name) if isSoftwareVectored { fn.SetLinkage(llvm.InternalLinkage) diff --git a/transform/interrupt_test.go b/transform/interrupt_test.go index 00a199063..60d83da15 100644 --- a/transform/interrupt_test.go +++ b/transform/interrupt_test.go @@ -11,7 +11,7 @@ func TestInterruptLowering(t *testing.T) { for _, subtest := range []string{"avr", "cortexm"} { t.Run(subtest, func(t *testing.T) { testTransform(t, "testdata/interrupt-"+subtest, func(mod llvm.Module) { - errs := LowerInterrupts(mod) + errs := LowerInterrupts(mod, 0) if len(errs) != 0 { t.Fail() for _, err := range errs { diff --git a/transform/optimizer.go b/transform/optimizer.go index e10144b92..108219106 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -76,12 +76,12 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i OptimizeStringToBytes(mod) OptimizeReflectImplements(mod) OptimizeAllocs(mod) - err := LowerInterfaces(mod) + err := LowerInterfaces(mod, sizeLevel) if err != nil { return []error{err} } - errs := LowerInterrupts(mod) + errs := LowerInterrupts(mod, sizeLevel) if len(errs) > 0 { return errs } @@ -102,14 +102,14 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i } else { // Must be run at any optimization level. - err := LowerInterfaces(mod) + err := LowerInterfaces(mod, sizeLevel) if err != nil { return []error{err} } if config.FuncImplementation() == "switch" { LowerFuncValues(mod) } - errs := LowerInterrupts(mod) + errs := LowerInterrupts(mod, sizeLevel) if len(errs) > 0 { return errs } @@ -153,16 +153,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i 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) diff --git a/transform/wasm-abi.go b/transform/wasm-abi.go index 0c3af9a3d..85e419145 100644 --- a/transform/wasm-abi.go +++ b/transform/wasm-abi.go @@ -73,6 +73,10 @@ func ExternalInt64AsPtr(mod llvm.Module) error { fn.SetName(name + "$i64wrap") externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg()) externalFn := llvm.AddFunction(mod, name, externalFnType) + optsize := fn.GetEnumFunctionAttribute(llvm.AttributeKindID("optsize")) + if !optsize.IsNil() { + fn.AddFunctionAttr(optsize) + } if fn.IsDeclaration() { // Just a declaration: the definition doesn't exist on the Go side |