Skip to content

Commit

Permalink
all: use the new LLVM pass manager
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aykevl committed Sep 19, 2023
1 parent 42da765 commit 1a56089
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 115 deletions.
19 changes: 8 additions & 11 deletions builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"),
)
}
Expand Down Expand Up @@ -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)
}
Expand Down
12 changes: 6 additions & 6 deletions compileopts/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "01", 1, 0
case "2":
return 2, 0, 225 // -O2
return "02", 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.
Expand Down
12 changes: 5 additions & 7 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Check failure on line 94 in compiler/compiler_test.go

View workflow job for this annotation

GitHub Actions / build-windows

undefined: llvm.NewPassBuilderOptions

Check failure on line 94 in compiler/compiler_test.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

undefined: llvm.NewPassBuilderOptions

Check failure on line 94 in compiler/compiler_test.go

View workflow job for this annotation

GitHub Actions / build-macos

undefined: llvm.NewPassBuilderOptions
defer passOptions.Dispose()
err = mod.RunPasses("instcombine", llvm.TargetMachine{}, passOptions)

Check failure on line 96 in compiler/compiler_test.go

View workflow job for this annotation

GitHub Actions / build-windows

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 96 in compiler/compiler_test.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 96 in compiler/compiler_test.go

View workflow job for this annotation

GitHub Actions / build-macos

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)
if err != nil {
t.Error(err)
}
funcPasses.FinalizeFunc()

outFilePrefix := tc.file[:len(tc.file)-3]
if tc.target != "" {
Expand Down
9 changes: 3 additions & 6 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Check failure on line 82 in interp/interp_test.go

View workflow job for this annotation

GitHub Actions / build-windows

undefined: llvm.NewPassBuilderOptions

Check failure on line 82 in interp/interp_test.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

undefined: llvm.NewPassBuilderOptions

Check failure on line 82 in interp/interp_test.go

View workflow job for this annotation

GitHub Actions / build-macos

undefined: 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")
Expand Down
11 changes: 6 additions & 5 deletions transform/allocs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions transform/interface-lowering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
11 changes: 6 additions & 5 deletions transform/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
102 changes: 32 additions & 70 deletions transform/optimizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()

Check failure on line 57 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

undefined: llvm.NewPassBuilderOptions

Check failure on line 57 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

undefined: llvm.NewPassBuilderOptions

Check failure on line 57 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

undefined: llvm.NewPassBuilderOptions

Check failure on line 57 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

undefined: llvm.NewPassBuilderOptions
defer po.Dispose()
err := mod.RunPasses("globaldce,globalopt,ipsccp,instcombine,adce,function-attrs", llvm.TargetMachine{}, po)

Check failure on line 59 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 59 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 59 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 59 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)
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}
}
Expand All @@ -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)

Check failure on line 81 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 81 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 81 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 81 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)
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) {
Expand All @@ -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()

Check failure on line 105 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

undefined: llvm.NewPassBuilderOptions

Check failure on line 105 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

undefined: llvm.NewPassBuilderOptions

Check failure on line 105 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

undefined: llvm.NewPassBuilderOptions

Check failure on line 105 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

undefined: llvm.NewPassBuilderOptions
defer po.Dispose()
err = mod.RunPasses("globaldce", llvm.TargetMachine{}, po)

Check failure on line 107 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 107 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 107 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 107 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)
if err != nil {
return []error{fmt.Errorf("could not build pass pipeline: %w", err)}
}
}

if config.Scheduler() == "none" {
Expand Down Expand Up @@ -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()

Check failure on line 144 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

undefined: llvm.NewPassBuilderOptions

Check failure on line 144 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

undefined: llvm.NewPassBuilderOptions

Check failure on line 144 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

undefined: llvm.NewPassBuilderOptions

Check failure on line 144 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

undefined: llvm.NewPassBuilderOptions
defer po.Dispose()
passes := fmt.Sprintf("default<%s>", optLevel)
err := mod.RunPasses(passes, llvm.TargetMachine{}, po)

Check failure on line 147 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-windows

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 147 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / homebrew-install

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 147 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / assert-test-linux

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)

Check failure on line 147 in transform/optimizer.go

View workflow job for this annotation

GitHub Actions / build-macos

mod.RunPasses undefined (type llvm.Module has no field or method RunPasses)
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 {
Expand Down
2 changes: 1 addition & 1 deletion transform/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down

0 comments on commit 1a56089

Please sign in to comment.