aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2023-09-19 22:37:44 +0200
committerRon Evans <[email protected]>2023-10-04 13:05:58 +0200
commit3b1913ac57420af2c665c6f1c3847a6e63774ecd (patch)
tree77460ae8c35853f7f16d4f8575576e677d78e737
parent1da1abe3147796aa56a5486ed6f07afdd88d8234 (diff)
downloadtinygo-3b1913ac57420af2c665c6f1c3847a6e63774ecd.tar.gz
tinygo-3b1913ac57420af2c665c6f1c3847a6e63774ecd.zip
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.
-rw-r--r--builder/build.go19
-rw-r--r--builder/sizes_test.go6
-rw-r--r--compileopts/config.go12
-rw-r--r--compiler/compiler_test.go12
-rw-r--r--interp/interp_test.go9
-rw-r--r--transform/allocs_test.go11
-rw-r--r--transform/interface-lowering_test.go10
-rw-r--r--transform/maps_test.go11
-rw-r--r--transform/optimizer.go102
-rw-r--r--transform/transform.go2
10 files changed, 76 insertions, 118 deletions
diff --git a/builder/build.go b/builder/build.go
index 136669a1c..1f40ab3f6 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 {
@@ -743,17 +741,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"),
)
}
@@ -1066,10 +1064,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/builder/sizes_test.go b/builder/sizes_test.go
index 7aaab78a5..dc45898ec 100644
--- a/builder/sizes_test.go
+++ b/builder/sizes_test.go
@@ -41,9 +41,9 @@ func TestBinarySize(t *testing.T) {
// This is a small number of very diverse targets that we want to test.
tests := []sizeTest{
// microcontrollers
- {"hifive1b", "examples/echo", 4568, 280, 0, 2252},
- {"microbit", "examples/serial", 2728, 388, 8, 2256},
- {"wioterminal", "examples/pininterrupt", 5996, 1484, 116, 6816},
+ {"hifive1b", "examples/echo", 4484, 280, 0, 2252},
+ {"microbit", "examples/serial", 2724, 388, 8, 2256},
+ {"wioterminal", "examples/pininterrupt", 6000, 1484, 116, 6816},
// TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the
diff --git a/compileopts/config.go b/compileopts/config.go
index 39fc4f2ac..5ad45c607 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 fcb2dd7e2..fc62e98e8 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/interp/interp_test.go b/interp/interp_test.go
index fc567af20..cac565087 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -77,12 +77,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 27bb9706a..59a5b14e2 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 7bcce6055..65f14dd95 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 e8b111337..329de698e 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 20258ef4f..42acc2ddc 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 ab08317e1..429cbd5f3 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))
}