From 3da1b40934c3c695f7054d13b7d480e74e286465 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 19 Sep 2023 22:37:44 +0200 Subject: [PATCH] all: use the new LLVM pass manager The old LLVM pass manager is deprecated and should not be used anymore. Moreover, the pass manager builder (which we used to set up a pass pipeline) is actually removed from LLVM entirely in LLVM 17: https://reviews.llvm.org/D145387 https://reviews.llvm.org/D145835 The new pass manager does change the binary size in many cases: both growing and shrinking it. However, on average the binary size remains more or less the same. This is needed as a preparation for LLVM 17. --- builder/build.go | 19 +++-- compileopts/config.go | 12 ++-- compiler/compiler_test.go | 12 ++-- go.mod | 2 +- go.sum | 4 +- interp/interp_test.go | 9 +-- transform/allocs_test.go | 11 +-- transform/interface-lowering_test.go | 10 +-- transform/maps_test.go | 11 +-- transform/optimizer.go | 102 +++++++++------------------ transform/transform.go | 2 +- 11 files changed, 76 insertions(+), 118 deletions(-) diff --git a/builder/build.go b/builder/build.go index 85114d9d72..9e55a59083 100644 --- a/builder/build.go +++ b/builder/build.go @@ -83,8 +83,7 @@ type packageAction struct { FileHashes map[string]string // hash of every file that's part of the package EmbeddedFiles map[string]string // hash of all the //go:embed files in the package Imports map[string]string // map from imported package to action ID hash - OptLevel int // LLVM optimization level (0-3) - SizeLevel int // LLVM optimization for size level (0-2) + OptLevel string // LLVM optimization level (O0, O1, O2, Os, Oz) UndefinedGlobals []string // globals that are left as external globals (no initializer) } @@ -158,7 +157,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe return BuildResult{}, fmt.Errorf("unknown libc: %s", config.Target.Libc) } - optLevel, sizeLevel, _ := config.OptLevels() + optLevel, speedLevel, sizeLevel := config.OptLevel() compilerConfig := &compiler.Config{ Triple: config.Triple(), CPU: config.CPU(), @@ -321,7 +320,6 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe EmbeddedFiles: make(map[string]string, len(allFiles)), Imports: make(map[string]string, len(pkg.Pkg.Imports())), OptLevel: optLevel, - SizeLevel: sizeLevel, UndefinedGlobals: undefinedGlobals, } for filePath, hash := range pkg.FileHashes { @@ -739,17 +737,17 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if config.GOOS() == "windows" { // Options for the MinGW wrapper for the lld COFF linker. ldflags = append(ldflags, - "-Xlink=/opt:lldlto="+strconv.Itoa(optLevel), + "-Xlink=/opt:lldlto="+strconv.Itoa(speedLevel), "--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto")) } else if config.GOOS() == "darwin" { // Options for the ld64-compatible lld linker. ldflags = append(ldflags, - "--lto-O"+strconv.Itoa(optLevel), + "--lto-O"+strconv.Itoa(speedLevel), "-cache_path_lto", filepath.Join(cacheDir, "thinlto")) } else { // Options for the ELF linker. ldflags = append(ldflags, - "--lto-O"+strconv.Itoa(optLevel), + "--lto-O"+strconv.Itoa(speedLevel), "--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto"), ) } @@ -1062,10 +1060,9 @@ func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { return err } - // Optimization levels here are roughly the same as Clang, but probably not - // exactly. - optLevel, sizeLevel, inlinerThreshold := config.OptLevels() - errs := transform.Optimize(mod, config, optLevel, sizeLevel, inlinerThreshold) + // Run most of the whole-program optimizations (including the whole + // O0/O1/O2/Os/Oz optimization pipeline). + errs := transform.Optimize(mod, config) if len(errs) > 0 { return newMultiError(errs) } diff --git a/compileopts/config.go b/compileopts/config.go index 39fc4f2ac2..5ad45c6078 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -145,18 +145,18 @@ func (c *Config) Serial() string { // 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) { +func (c *Config) OptLevel() (level string, speedLevel, sizeLevel int) { switch c.Options.Opt { case "none", "0": - return 0, 0, 0 // -O0 + return "O0", 0, 0 case "1": - return 1, 0, 0 // -O1 + return "O1", 1, 0 case "2": - return 2, 0, 225 // -O2 + return "O2", 2, 0 case "s": - return 2, 1, 225 // -Os + return "Os", 2, 1 case "z": - return 2, 2, 5 // -Oz, default + return "Oz", 2, 2 // 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. diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 92ce31b012..147e622a45 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -91,14 +91,12 @@ func TestCompiler(t *testing.T) { } // Optimize IR a little. - funcPasses := llvm.NewFunctionPassManagerForModule(mod) - defer funcPasses.Dispose() - funcPasses.AddInstructionCombiningPass() - funcPasses.InitializeFunc() - for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - funcPasses.RunFunc(fn) + passOptions := llvm.NewPassBuilderOptions() + defer passOptions.Dispose() + err = mod.RunPasses("instcombine", llvm.TargetMachine{}, passOptions) + if err != nil { + t.Error(err) } - funcPasses.FinalizeFunc() outFilePrefix := tc.file[:len(tc.file)-3] if tc.target != "" { diff --git a/go.mod b/go.mod index 9af3402bad..c7fe95d86b 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sys v0.11.0 golang.org/x/tools v0.12.0 gopkg.in/yaml.v2 v2.4.0 - tinygo.org/x/go-llvm v0.0.0-20230918183930-9edb6403d0bc + tinygo.org/x/go-llvm v0.0.0-20230920204606-773369585384 ) require ( diff --git a/go.sum b/go.sum index 587efdf2ac..54246f6e41 100644 --- a/go.sum +++ b/go.sum @@ -65,5 +65,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -tinygo.org/x/go-llvm v0.0.0-20230918183930-9edb6403d0bc h1:IVX1dqCX3c88P7iEMBtz1xCAM4UIqCMgbqHdSefBaWE= -tinygo.org/x/go-llvm v0.0.0-20230918183930-9edb6403d0bc/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= +tinygo.org/x/go-llvm v0.0.0-20230920204606-773369585384 h1:dZk1zTg/oBTErUNl3gqar+LIVxxbMmhdy+cJJKEBeKo= +tinygo.org/x/go-llvm v0.0.0-20230920204606-773369585384/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/interp/interp_test.go b/interp/interp_test.go index 55ad3eaef6..b296b00cff 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -79,12 +79,9 @@ func runTest(t *testing.T, pathPrefix string) { } // Run some cleanup passes to get easy-to-read outputs. - pm := llvm.NewPassManager() - defer pm.Dispose() - pm.AddGlobalOptimizerPass() - pm.AddDeadStoreEliminationPass() - pm.AddAggressiveDCEPass() - pm.Run(mod) + to := llvm.NewPassBuilderOptions() + defer to.Dispose() + mod.RunPasses("globalopt,dse,adce", llvm.TargetMachine{}, to) // Read the expected output IR. out, err := os.ReadFile(pathPrefix + ".out.ll") diff --git a/transform/allocs_test.go b/transform/allocs_test.go index 27bb9706a1..59a5b14e24 100644 --- a/transform/allocs_test.go +++ b/transform/allocs_test.go @@ -38,11 +38,12 @@ func TestAllocs2(t *testing.T) { mod := compileGoFileForTesting(t, "./testdata/allocs2.go") // Run functionattrs pass, which is necessary for escape analysis. - pm := llvm.NewPassManager() - defer pm.Dispose() - pm.AddInstructionCombiningPass() - pm.AddFunctionAttrsPass() - pm.Run(mod) + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + err := mod.RunPasses("function(instcombine),function-attrs", llvm.TargetMachine{}, po) + if err != nil { + t.Error("failed to run passes:", err) + } // Run heap to stack transform. var testOutputs []allocsTestOutput diff --git a/transform/interface-lowering_test.go b/transform/interface-lowering_test.go index 7bcce6055c..65f14dd95e 100644 --- a/transform/interface-lowering_test.go +++ b/transform/interface-lowering_test.go @@ -15,9 +15,11 @@ func TestInterfaceLowering(t *testing.T) { t.Error(err) } - pm := llvm.NewPassManager() - defer pm.Dispose() - pm.AddGlobalDCEPass() - pm.Run(mod) + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + err = mod.RunPasses("globaldce", llvm.TargetMachine{}, po) + if err != nil { + t.Error("failed to run passes:", err) + } }) } diff --git a/transform/maps_test.go b/transform/maps_test.go index e8b1113374..329de698e2 100644 --- a/transform/maps_test.go +++ b/transform/maps_test.go @@ -15,10 +15,11 @@ func TestOptimizeMaps(t *testing.T) { // Run an optimization pass, to clean up the result. // This shows that all code related to the map is really eliminated. - pm := llvm.NewPassManager() - defer pm.Dispose() - pm.AddDeadStoreEliminationPass() - pm.AddAggressiveDCEPass() - pm.Run(mod) + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + err := mod.RunPasses("dse,adce", llvm.TargetMachine{}, po) + if err != nil { + t.Error("failed to run passes:", err) + } }) } diff --git a/transform/optimizer.go b/transform/optimizer.go index 20258ef4fe..42acc2ddce 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -14,54 +14,22 @@ import ( // OptimizePackage runs optimization passes over the LLVM module for the given // Go package. func OptimizePackage(mod llvm.Module, config *compileopts.Config) { - optLevel, sizeLevel, _ := config.OptLevels() - - // Run function passes for each function in the module. - // These passes are intended to be run on each function right - // after they're created to reduce IR size (and maybe also for - // cache locality to improve performance), but for now they're - // run here for each function in turn. Maybe this can be - // improved in the future. - builder := llvm.NewPassManagerBuilder() - defer builder.Dispose() - builder.SetOptLevel(optLevel) - builder.SetSizeLevel(sizeLevel) - funcPasses := llvm.NewFunctionPassManagerForModule(mod) - defer funcPasses.Dispose() - builder.PopulateFunc(funcPasses) - funcPasses.InitializeFunc() - for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - if fn.IsDeclaration() { - continue - } - funcPasses.RunFunc(fn) - } - funcPasses.FinalizeFunc() + _, speedLevel, _ := config.OptLevel() // Run TinyGo-specific optimization passes. - if optLevel > 0 { + if speedLevel > 0 { OptimizeMaps(mod) } } // 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). +// passes. // // 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) - } +// alwasy be run before emitting machine code. +func Optimize(mod llvm.Module, config *compileopts.Config) []error { + optLevel, speedLevel, _ := config.OptLevel() // Make sure these functions are kept in tact during TinyGo transformation passes. for _, name := range functionsUsedInTransforms { @@ -84,23 +52,20 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i } } - if optLevel > 0 { + if speedLevel > 0 { // Run some preparatory passes for the Go optimizer. - goPasses := llvm.NewPassManager() - defer goPasses.Dispose() - goPasses.AddGlobalDCEPass() - goPasses.AddGlobalOptimizerPass() - goPasses.AddIPSCCPPass() - goPasses.AddInstructionCombiningPass() // necessary for OptimizeReflectImplements - goPasses.AddAggressiveDCEPass() - goPasses.AddFunctionAttrsPass() - goPasses.Run(mod) + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + err := mod.RunPasses("globaldce,globalopt,ipsccp,instcombine,adce,function-attrs", llvm.TargetMachine{}, po) + if err != nil { + return []error{fmt.Errorf("could not build pass pipeline: %w", err)} + } // Run TinyGo-specific optimization passes. OptimizeStringToBytes(mod) OptimizeReflectImplements(mod) OptimizeAllocs(mod, nil, nil) - err := LowerInterfaces(mod, config) + err = LowerInterfaces(mod, config) if err != nil { return []error{err} } @@ -113,7 +78,10 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i // 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) + err = mod.RunPasses("globaldce,globalopt,ipsccp,instcombine,adce,function-attrs", llvm.TargetMachine{}, po) + if err != nil { + return []error{fmt.Errorf("could not build pass pipeline: %w", err)} + } // Run TinyGo-specific interprocedural optimizations. OptimizeAllocs(mod, config.Options.PrintAllocs, func(pos token.Position, msg string) { @@ -134,10 +102,12 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i } // Clean up some leftover symbols of the previous transformations. - goPasses := llvm.NewPassManager() - defer goPasses.Dispose() - goPasses.AddGlobalDCEPass() - goPasses.Run(mod) + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + err = mod.RunPasses("globaldce", llvm.TargetMachine{}, po) + if err != nil { + return []error{fmt.Errorf("could not build pass pipeline: %w", err)} + } } if config.Scheduler() == "none" { @@ -169,23 +139,15 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i fn.SetLinkage(llvm.InternalLinkage) } - // Run function passes again, because without it, llvm.coro.size.i32() - // doesn't get lowered. - 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) + // Run the default pass pipeline. + // TODO: set the PrepareForThinLTO flag somehow. + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + passes := fmt.Sprintf("default<%s>", optLevel) + err := mod.RunPasses(passes, llvm.TargetMachine{}, po) + if err != nil { + return []error{fmt.Errorf("could not build pass pipeline: %w", err)} } - funcPasses.FinalizeFunc() - - // Run module passes. - // TODO: somehow set the PrepareForThinLTO flag in the pass manager builder. - modPasses := llvm.NewPassManager() - defer modPasses.Dispose() - builder.Populate(modPasses) - modPasses.Run(mod) hasGCPass := MakeGCStackSlots(mod) if hasGCPass { diff --git a/transform/transform.go b/transform/transform.go index ab08317e17..429cbd5f30 100644 --- a/transform/transform.go +++ b/transform/transform.go @@ -22,7 +22,7 @@ import ( // the -opt= compiler flag. func AddStandardAttributes(fn llvm.Value, config *compileopts.Config) { ctx := fn.Type().Context() - _, sizeLevel, _ := config.OptLevels() + _, _, sizeLevel := config.OptLevel() if sizeLevel >= 1 { fn.AddFunctionAttr(ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0)) }