build: support optimization flags for in-memory compilation#1836
build: support optimization flags for in-memory compilation#1836xushiwei merged 2 commits intoxgo-dev:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive optimization level system, adding a dedicated optlevel package and command-line flags (-O0 to -Oz). These levels are integrated into the build configuration, cross-compilation flags, and the LLVM optimization pipeline. Review feedback identifies critical compilation errors in the test suite due to the use of an undefined function UseWithOptLevel. Additionally, suggestions were made to prevent potential errors when an empty optimization pipeline is returned and to centralize duplicated logic used to determine default optimization levels.
| func TestUseWithTarget(t *testing.T) { | ||
| // Test target-based configuration takes precedence | ||
| export, err := Use("linux", "amd64", "esp32", false, true) | ||
| export, err := UseWithOptLevel("linux", "amd64", "esp32", false, true, optlevel.Oz) |
There was a problem hiding this comment.
The function UseWithOptLevel is not defined in the crosscompile package. The Use function was updated to accept an optimization level as its final argument, so it should be used here instead. This will cause a compilation error in the tests.
| export, err := UseWithOptLevel("linux", "amd64", "esp32", false, true, optlevel.Oz) | |
| export, err := Use("linux", "amd64", "esp32", false, true, optlevel.Oz) |
|
|
||
| // Test fallback to goos/goarch when no target specified | ||
| export, err = Use(runtime.GOOS, runtime.GOARCH, "", false, false) | ||
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O2) |
There was a problem hiding this comment.
| t.Fatalf("target CCFLAGS = %v, want first flag -Oz", export.CCFLAGS) | ||
| } | ||
|
|
||
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O3) |
There was a problem hiding this comment.
| if ctx.passOpt { | ||
| mod := ret.Module() | ||
| mod.SetDataLayout(ctx.prog.DataLayout()) | ||
| mod.SetTarget(ctx.prog.Target().Spec().Triple) | ||
| pbo := gllvm.NewPassBuilderOptions() | ||
| defer pbo.Dispose() | ||
| if err := mod.RunPasses("memcpyopt", ctx.prog.TargetMachine(), pbo); err != nil { | ||
| if err := mod.RunPasses(llvmPassPipeline(ctx.buildConf.OptLevel), ctx.prog.TargetMachine(), pbo); err != nil { | ||
| return fmt.Errorf("run LLVM passes failed for %v: %v", pkgPath, err) | ||
| } | ||
| } |
There was a problem hiding this comment.
If ctx.buildConf.OptLevel is O0, llvmPassPipeline returns an empty string. Calling mod.RunPasses with an empty pipeline string may cause an error in the LLVM pass builder or result in a no-op. It is safer to skip the optimization passes entirely when the pipeline string is empty.
| if ctx.passOpt { | |
| mod := ret.Module() | |
| mod.SetDataLayout(ctx.prog.DataLayout()) | |
| mod.SetTarget(ctx.prog.Target().Spec().Triple) | |
| pbo := gllvm.NewPassBuilderOptions() | |
| defer pbo.Dispose() | |
| if err := mod.RunPasses("memcpyopt", ctx.prog.TargetMachine(), pbo); err != nil { | |
| if err := mod.RunPasses(llvmPassPipeline(ctx.buildConf.OptLevel), ctx.prog.TargetMachine(), pbo); err != nil { | |
| return fmt.Errorf("run LLVM passes failed for %v: %v", pkgPath, err) | |
| } | |
| } | |
| if ctx.passOpt { | |
| if pipeline := llvmPassPipeline(ctx.buildConf.OptLevel); pipeline != "" { | |
| mod := ret.Module() | |
| mod.SetDataLayout(ctx.prog.DataLayout()) | |
| mod.SetTarget(ctx.prog.Target().Spec().Triple) | |
| pbo := gllvm.NewPassBuilderOptions() | |
| defer pbo.Dispose() | |
| if err := mod.RunPasses(pipeline, ctx.prog.TargetMachine(), pbo); err != nil { | |
| return fmt.Errorf("run LLVM passes failed for %v: %v", pkgPath, err) | |
| } | |
| } | |
| } |
| func (p *Target) effectiveOptLevel() optlevel.Level { | ||
| if p != nil && p.OptLevel.IsValid() { | ||
| return p.OptLevel | ||
| } | ||
| if p != nil && p.Target != "" { | ||
| return optlevel.Oz | ||
| } | ||
| return optlevel.O2 | ||
| } |
There was a problem hiding this comment.
There was a problem hiding this comment.
Review Summary
Well-structured PR that cleanly introduces optimization level handling through a dedicated internal/optlevel package and wires it through the full build pipeline. The enum-based design with allowlist validation is solid from both a correctness and security standpoint. Good test coverage across the new and modified packages.
There are a few issues to address — one compilation-breaking bug in the tests and one likely runtime issue with the -O0 path.
Issues Found
| Severity | Count |
|---|---|
| Critical | 2 |
| Medium | 2 |
| Low | 2 |
| func TestUseWithTarget(t *testing.T) { | ||
| // Test target-based configuration takes precedence | ||
| export, err := Use("linux", "amd64", "esp32", false, true) | ||
| export, err := UseWithOptLevel("linux", "amd64", "esp32", false, true, optlevel.Oz) |
There was a problem hiding this comment.
Bug (build-breaking): UseWithOptLevel is called here but does not exist. The renamed function is Use(goos, goarch, targetName, wasiThreads, forceEspClang, level). This (and the other two calls on lines 328 and 348) should use Use(...) instead.
| export, err := UseWithOptLevel("linux", "amd64", "esp32", false, true, optlevel.Oz) | |
| export, err := Use("linux", "amd64", "esp32", false, true, optlevel.Oz) |
|
|
||
| // Test fallback to goos/goarch when no target specified | ||
| export, err = Use(runtime.GOOS, runtime.GOARCH, "", false, false) | ||
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O2) |
There was a problem hiding this comment.
Same issue — UseWithOptLevel → Use.
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O2) | |
| export, err = Use(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O2) |
| t.Fatalf("target CCFLAGS = %v, want first flag -Oz", export.CCFLAGS) | ||
| } | ||
|
|
||
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O3) |
There was a problem hiding this comment.
Same issue — UseWithOptLevel → Use.
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O3) | |
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O3) |
Should be:
| export, err = UseWithOptLevel(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O3) | |
| export, err = Use(runtime.GOOS, runtime.GOARCH, "", false, false, optlevel.O3) |
| pbo := gllvm.NewPassBuilderOptions() | ||
| defer pbo.Dispose() | ||
| if err := mod.RunPasses("memcpyopt", ctx.prog.TargetMachine(), pbo); err != nil { | ||
| if err := mod.RunPasses(llvmPassPipeline(ctx.buildConf.OptLevel), ctx.prog.TargetMachine(), pbo); err != nil { |
There was a problem hiding this comment.
Potential runtime error at -O0: When OptLevel is O0, llvmPassPipeline returns "". Since ctx.passOpt is still true (it's only disabled for debug/gen mode, not for O0), this will call mod.RunPasses("", ...) with an empty pipeline string. Depending on the LLVM version, this may error out.
Consider guarding:
pipeline := llvmPassPipeline(ctx.buildConf.OptLevel)
if ctx.passOpt && pipeline != "" {Or skip the unnecessary PassBuilderOptions allocation entirely when there's nothing to run.
| return machine.CreateTargetData(), machine | ||
| } | ||
|
|
||
| func (p *Target) effectiveOptLevel() optlevel.Level { |
There was a problem hiding this comment.
Duplicated fallback logic: This effectiveOptLevel mirrors internal/build/build.go's version, but diverges: the build.go version also consults the LLGO_OPTIMIZE env var, while this one does not. Since build.Do() already resolves the effective level and sets Target.OptLevel before reaching this code, this function is largely redundant in the normal flow. Consider either:
- Simplifying this to just
return p.OptLevel(trusting the caller), or - Extracting the shared fallback into the
optlevelpackage
At minimum, a doc comment explaining why the ssa version intentionally omits the env-var check would prevent future confusion.
| spec.CPU, | ||
| spec.Features, | ||
| llvm.CodeGenLevelNone, | ||
| p.codeGenOptLevel(), |
There was a problem hiding this comment.
Behavioral change worth noting: Previously this was hardcoded to llvm.CodeGenLevelNone. Now the default (when no explicit level is set and there's no cross-compile target) resolves to O2 → CodeGenLevelDefault. This will change backend codegen behavior for all existing users who haven't explicitly set an optimization level — potentially increasing build times while improving generated code quality. Worth calling out in the PR description or changelog.
60c696d to
b210c40
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1836 +/- ##
==========================================
+ Coverage 88.50% 88.54% +0.04%
==========================================
Files 50 51 +1
Lines 14426 14496 +70
==========================================
+ Hits 12768 12836 +68
- Misses 1438 1442 +4
+ Partials 220 218 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- add reusable parsing for -O0/-O1/-O2/-O3/-Os/-Oz and -O=<level>, with mutual exclusion checks - apply optimization levels to in-memory LLVM codegen, target-based cross compilation, and monitor target lookup - keep legacy LLGO_OPTIMIZE handling separate from explicit -O* selection and extend related tests Signed-off-by: ZhouGuangyuan <zhouguangyuan.xian@gmail.com>
b210c40 to
1afc955
Compare
- allow FileCheck function definition lines to match inserted function attributes - update cl lit expectations across _testdata, _testgo, _testrt and related suites - refresh the sigsegv lit fixture after the optimization-level changes Signed-off-by: ZhouGuangyuan <zhouguangyuan.xian@gmail.com>
1afc955 to
22f1ec7
Compare
Summary
This adds explicit optimization level support to the current in-memory compilation flow.
It revives the intent of #1762, but adapts the implementation to the native LLVM codegen path instead of only forwarding
-O*to a final clang invocation.What changed
-O0/-O1/-O2/-O3/-Os/-Ozand-O=<level>build.Config-O*handling separate from the legacyLLGO_OPTIMIZE/IsOptimizeEnabledpathllgo monitoraccept the same-O*flags and default target lookup toOzTest updates
clFileCheck expectations for functiondefinelines so inserted function attributes do not break matchingcl/_testgo/sigsegvlit fixture after the optimization changesTesting
go test ./internal/optlevel ./cmd/internal/flags ./internal/build ./internal/crosscompile ./ssa -count=1go test ./cmd/internal/flags ./cmd/internal/monitor -count=1go test ./cl -run 'TestRunAndTestFromTestdata/(method|print|embedunexport|geometry1370)' -count=1go test ./cl -run 'TestRunAndTestFromTestrt/(qsort|struct|vamethod|builtin)' -count=1go test ./cl -run 'TestRunAndTestFromTestgo/(invoke|tpnamed|reader|closureall)' -count=1