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.
Этот коммит содержится в:
Ayke van Laethem 2023-09-19 22:37:44 +02:00 коммит произвёл Ron Evans
родитель 1da1abe314
коммит 3b1913ac57
10 изменённых файлов: 76 добавлений и 118 удалений

Просмотреть файл

@ -83,8 +83,7 @@ type packageAction struct {
FileHashes map[string]string // hash of every file that's part of the package 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 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 Imports map[string]string // map from imported package to action ID hash
OptLevel int // LLVM optimization level (0-3) OptLevel string // LLVM optimization level (O0, O1, O2, Os, Oz)
SizeLevel int // LLVM optimization for size level (0-2)
UndefinedGlobals []string // globals that are left as external globals (no initializer) 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) return BuildResult{}, fmt.Errorf("unknown libc: %s", config.Target.Libc)
} }
optLevel, sizeLevel, _ := config.OptLevels() optLevel, speedLevel, sizeLevel := config.OptLevel()
compilerConfig := &compiler.Config{ compilerConfig := &compiler.Config{
Triple: config.Triple(), Triple: config.Triple(),
CPU: config.CPU(), CPU: config.CPU(),
@ -321,7 +320,6 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
EmbeddedFiles: make(map[string]string, len(allFiles)), EmbeddedFiles: make(map[string]string, len(allFiles)),
Imports: make(map[string]string, len(pkg.Pkg.Imports())), Imports: make(map[string]string, len(pkg.Pkg.Imports())),
OptLevel: optLevel, OptLevel: optLevel,
SizeLevel: sizeLevel,
UndefinedGlobals: undefinedGlobals, UndefinedGlobals: undefinedGlobals,
} }
for filePath, hash := range pkg.FileHashes { for filePath, hash := range pkg.FileHashes {
@ -743,17 +741,17 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
if config.GOOS() == "windows" { if config.GOOS() == "windows" {
// Options for the MinGW wrapper for the lld COFF linker. // Options for the MinGW wrapper for the lld COFF linker.
ldflags = append(ldflags, ldflags = append(ldflags,
"-Xlink=/opt:lldlto="+strconv.Itoa(optLevel), "-Xlink=/opt:lldlto="+strconv.Itoa(speedLevel),
"--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto")) "--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto"))
} else if config.GOOS() == "darwin" { } else if config.GOOS() == "darwin" {
// Options for the ld64-compatible lld linker. // Options for the ld64-compatible lld linker.
ldflags = append(ldflags, ldflags = append(ldflags,
"--lto-O"+strconv.Itoa(optLevel), "--lto-O"+strconv.Itoa(speedLevel),
"-cache_path_lto", filepath.Join(cacheDir, "thinlto")) "-cache_path_lto", filepath.Join(cacheDir, "thinlto"))
} else { } else {
// Options for the ELF linker. // Options for the ELF linker.
ldflags = append(ldflags, ldflags = append(ldflags,
"--lto-O"+strconv.Itoa(optLevel), "--lto-O"+strconv.Itoa(speedLevel),
"--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto"), "--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto"),
) )
} }
@ -1066,10 +1064,9 @@ func optimizeProgram(mod llvm.Module, config *compileopts.Config) error {
return err return err
} }
// Optimization levels here are roughly the same as Clang, but probably not // Run most of the whole-program optimizations (including the whole
// exactly. // O0/O1/O2/Os/Oz optimization pipeline).
optLevel, sizeLevel, inlinerThreshold := config.OptLevels() errs := transform.Optimize(mod, config)
errs := transform.Optimize(mod, config, optLevel, sizeLevel, inlinerThreshold)
if len(errs) > 0 { if len(errs) > 0 {
return newMultiError(errs) return newMultiError(errs)
} }

Просмотреть файл

@ -41,9 +41,9 @@ func TestBinarySize(t *testing.T) {
// This is a small number of very diverse targets that we want to test. // This is a small number of very diverse targets that we want to test.
tests := []sizeTest{ tests := []sizeTest{
// microcontrollers // microcontrollers
{"hifive1b", "examples/echo", 4568, 280, 0, 2252}, {"hifive1b", "examples/echo", 4484, 280, 0, 2252},
{"microbit", "examples/serial", 2728, 388, 8, 2256}, {"microbit", "examples/serial", 2724, 388, 8, 2256},
{"wioterminal", "examples/pininterrupt", 5996, 1484, 116, 6816}, {"wioterminal", "examples/pininterrupt", 6000, 1484, 116, 6816},
// TODO: also check wasm. Right now this is difficult, because // TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the // wasm binaries are run through wasm-opt and therefore the

Просмотреть файл

@ -145,18 +145,18 @@ func (c *Config) Serial() string {
// OptLevels returns the optimization level (0-2), size level (0-2), and inliner // OptLevels returns the optimization level (0-2), size level (0-2), and inliner
// threshold as used in the LLVM optimization pipeline. // 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 { switch c.Options.Opt {
case "none", "0": case "none", "0":
return 0, 0, 0 // -O0 return "O0", 0, 0
case "1": case "1":
return 1, 0, 0 // -O1 return "O1", 1, 0
case "2": case "2":
return 2, 0, 225 // -O2 return "O2", 2, 0
case "s": case "s":
return 2, 1, 225 // -Os return "Os", 2, 1
case "z": case "z":
return 2, 2, 5 // -Oz, default return "Oz", 2, 2 // default
default: default:
// This is not shown to the user: valid choices are already checked as // This is not shown to the user: valid choices are already checked as
// part of Options.Verify(). It is here as a sanity check. // part of Options.Verify(). It is here as a sanity check.

Просмотреть файл

@ -91,14 +91,12 @@ func TestCompiler(t *testing.T) {
} }
// Optimize IR a little. // Optimize IR a little.
funcPasses := llvm.NewFunctionPassManagerForModule(mod) passOptions := llvm.NewPassBuilderOptions()
defer funcPasses.Dispose() defer passOptions.Dispose()
funcPasses.AddInstructionCombiningPass() err = mod.RunPasses("instcombine", llvm.TargetMachine{}, passOptions)
funcPasses.InitializeFunc() if err != nil {
for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { t.Error(err)
funcPasses.RunFunc(fn)
} }
funcPasses.FinalizeFunc()
outFilePrefix := tc.file[:len(tc.file)-3] outFilePrefix := tc.file[:len(tc.file)-3]
if tc.target != "" { if tc.target != "" {

Просмотреть файл

@ -77,12 +77,9 @@ func runTest(t *testing.T, pathPrefix string) {
} }
// Run some cleanup passes to get easy-to-read outputs. // Run some cleanup passes to get easy-to-read outputs.
pm := llvm.NewPassManager() to := llvm.NewPassBuilderOptions()
defer pm.Dispose() defer to.Dispose()
pm.AddGlobalOptimizerPass() mod.RunPasses("globalopt,dse,adce", llvm.TargetMachine{}, to)
pm.AddDeadStoreEliminationPass()
pm.AddAggressiveDCEPass()
pm.Run(mod)
// Read the expected output IR. // Read the expected output IR.
out, err := os.ReadFile(pathPrefix + ".out.ll") out, err := os.ReadFile(pathPrefix + ".out.ll")

Просмотреть файл

@ -38,11 +38,12 @@ func TestAllocs2(t *testing.T) {
mod := compileGoFileForTesting(t, "./testdata/allocs2.go") mod := compileGoFileForTesting(t, "./testdata/allocs2.go")
// Run functionattrs pass, which is necessary for escape analysis. // Run functionattrs pass, which is necessary for escape analysis.
pm := llvm.NewPassManager() po := llvm.NewPassBuilderOptions()
defer pm.Dispose() defer po.Dispose()
pm.AddInstructionCombiningPass() err := mod.RunPasses("function(instcombine),function-attrs", llvm.TargetMachine{}, po)
pm.AddFunctionAttrsPass() if err != nil {
pm.Run(mod) t.Error("failed to run passes:", err)
}
// Run heap to stack transform. // Run heap to stack transform.
var testOutputs []allocsTestOutput var testOutputs []allocsTestOutput

Просмотреть файл

@ -15,9 +15,11 @@ func TestInterfaceLowering(t *testing.T) {
t.Error(err) t.Error(err)
} }
pm := llvm.NewPassManager() po := llvm.NewPassBuilderOptions()
defer pm.Dispose() defer po.Dispose()
pm.AddGlobalDCEPass() err = mod.RunPasses("globaldce", llvm.TargetMachine{}, po)
pm.Run(mod) if err != nil {
t.Error("failed to run passes:", err)
}
}) })
} }

Просмотреть файл

@ -15,10 +15,11 @@ func TestOptimizeMaps(t *testing.T) {
// Run an optimization pass, to clean up the result. // Run an optimization pass, to clean up the result.
// This shows that all code related to the map is really eliminated. // This shows that all code related to the map is really eliminated.
pm := llvm.NewPassManager() po := llvm.NewPassBuilderOptions()
defer pm.Dispose() defer po.Dispose()
pm.AddDeadStoreEliminationPass() err := mod.RunPasses("dse,adce", llvm.TargetMachine{}, po)
pm.AddAggressiveDCEPass() if err != nil {
pm.Run(mod) t.Error("failed to run passes:", err)
}
}) })
} }

Просмотреть файл

@ -14,54 +14,22 @@ import (
// OptimizePackage runs optimization passes over the LLVM module for the given // OptimizePackage runs optimization passes over the LLVM module for the given
// Go package. // Go package.
func OptimizePackage(mod llvm.Module, config *compileopts.Config) { func OptimizePackage(mod llvm.Module, config *compileopts.Config) {
optLevel, sizeLevel, _ := config.OptLevels() _, speedLevel, _ := config.OptLevel()
// 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()
// Run TinyGo-specific optimization passes. // Run TinyGo-specific optimization passes.
if optLevel > 0 { if speedLevel > 0 {
OptimizeMaps(mod) OptimizeMaps(mod)
} }
} }
// Optimize runs a number of optimization and transformation passes over the // Optimize runs a number of optimization and transformation passes over the
// given module. Some passes are specific to TinyGo, others are generic LLVM // 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 // passes.
// 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 // Please note that some optimizations are not optional, thus Optimize must
// alwasy be run before emitting machine code. Set all controls (optLevel, // alwasy be run before emitting machine code.
// sizeLevel, inlinerThreshold) to 0 to reduce the number of optimizations to a func Optimize(mod llvm.Module, config *compileopts.Config) []error {
// minimum. optLevel, speedLevel, _ := config.OptLevel()
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)
}
// Make sure these functions are kept in tact during TinyGo transformation passes. // Make sure these functions are kept in tact during TinyGo transformation passes.
for _, name := range functionsUsedInTransforms { 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. // Run some preparatory passes for the Go optimizer.
goPasses := llvm.NewPassManager() po := llvm.NewPassBuilderOptions()
defer goPasses.Dispose() defer po.Dispose()
goPasses.AddGlobalDCEPass() err := mod.RunPasses("globaldce,globalopt,ipsccp,instcombine,adce,function-attrs", llvm.TargetMachine{}, po)
goPasses.AddGlobalOptimizerPass() if err != nil {
goPasses.AddIPSCCPPass() return []error{fmt.Errorf("could not build pass pipeline: %w", err)}
goPasses.AddInstructionCombiningPass() // necessary for OptimizeReflectImplements }
goPasses.AddAggressiveDCEPass()
goPasses.AddFunctionAttrsPass()
goPasses.Run(mod)
// Run TinyGo-specific optimization passes. // Run TinyGo-specific optimization passes.
OptimizeStringToBytes(mod) OptimizeStringToBytes(mod)
OptimizeReflectImplements(mod) OptimizeReflectImplements(mod)
OptimizeAllocs(mod, nil, nil) OptimizeAllocs(mod, nil, nil)
err := LowerInterfaces(mod, config) err = LowerInterfaces(mod, config)
if err != nil { if err != nil {
return []error{err} 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 // After interfaces are lowered, there are many more opportunities for
// interprocedural optimizations. To get them to work, function // interprocedural optimizations. To get them to work, function
// attributes have to be updated first. // 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. // Run TinyGo-specific interprocedural optimizations.
OptimizeAllocs(mod, config.Options.PrintAllocs, func(pos token.Position, msg string) { 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. // Clean up some leftover symbols of the previous transformations.
goPasses := llvm.NewPassManager() po := llvm.NewPassBuilderOptions()
defer goPasses.Dispose() defer po.Dispose()
goPasses.AddGlobalDCEPass() err = mod.RunPasses("globaldce", llvm.TargetMachine{}, po)
goPasses.Run(mod) if err != nil {
return []error{fmt.Errorf("could not build pass pipeline: %w", err)}
}
} }
if config.Scheduler() == "none" { if config.Scheduler() == "none" {
@ -169,23 +139,15 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i
fn.SetLinkage(llvm.InternalLinkage) fn.SetLinkage(llvm.InternalLinkage)
} }
// Run function passes again, because without it, llvm.coro.size.i32() // Run the default pass pipeline.
// doesn't get lowered. // TODO: set the PrepareForThinLTO flag somehow.
funcPasses := llvm.NewFunctionPassManagerForModule(mod) po := llvm.NewPassBuilderOptions()
defer funcPasses.Dispose() defer po.Dispose()
builder.PopulateFunc(funcPasses) passes := fmt.Sprintf("default<%s>", optLevel)
funcPasses.InitializeFunc() err := mod.RunPasses(passes, llvm.TargetMachine{}, po)
for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { if err != nil {
funcPasses.RunFunc(fn) 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) hasGCPass := MakeGCStackSlots(mod)
if hasGCPass { if hasGCPass {

Просмотреть файл

@ -22,7 +22,7 @@ import (
// the -opt= compiler flag. // the -opt= compiler flag.
func AddStandardAttributes(fn llvm.Value, config *compileopts.Config) { func AddStandardAttributes(fn llvm.Value, config *compileopts.Config) {
ctx := fn.Type().Context() ctx := fn.Type().Context()
_, sizeLevel, _ := config.OptLevels() _, _, sizeLevel := config.OptLevel()
if sizeLevel >= 1 { if sizeLevel >= 1 {
fn.AddFunctionAttr(ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0)) fn.AddFunctionAttr(ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0))
} }