Skip to content

benchmark: use PR merge-base for baseline comparison and add LTO workloads - #2373

Closed
cpunion wants to merge 7 commits into
xgo-dev:mainfrom
cpunion:codex/benchmark-merge-base-lto
Closed

benchmark: use PR merge-base for baseline comparison and add LTO workloads#2373
cpunion wants to merge 7 commits into
xgo-dev:mainfrom
cpunion:codex/benchmark-merge-base-lto

Conversation

@cpunion

@cpunion cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Note

Depends on #2371: This PR is built on top of #2371 (all: bump goplus/mod to 0.22.0, decouple loader, and fold composite literal static inits).

Summary

  1. Accurate Baseline via PR merge-base (.github/workflows/benchmark.yml):

    • Updates GitHub Actions benchmark workflow to compare PR measurements against the actual git merge-base common ancestor rather than the latest floating main HEAD.
    • Prevents external commits merged into main after branch creation from polluting or distorting PR performance and binary size diffs (base drift).
  2. LTO Workload Comparisons (benchmark/baseline/main.go):

    • Adds -lto=full compilation workloads for cprintf_lto, println_lto, and fmtprintf_lto alongside standard builds.
    • Extends workload definition to support arbitrary compiler flags.
    • Updates documentation and export test assertions.

Verification

  • go test -v ./benchmark/baseline (PASS)
  • bash test/buildcache/test.sh (18/18 PASS)

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

The benchmark changes themselves are sound: PR merge-base baseline selection avoids base drift, and the -lto=full workloads extend the workload struct cleanly (buildArgs is built once per workload and correctly does not alias item.flags). The bulk of this diff, however, is the internal/packages/load.go loader rewrite and the cl/static_init.go composite-literal static-init work carried in from the #2371 dependency. Those are where the review concerns concentrate.

Verified before flagging:

  • The compiler's own path (internal/build/build.go:454) sets packages.NeedDeps, so normal builds are unaffected by the loader change.
  • benchmark/baseline/README.md (default and -lto=full) notes accurately match the new workloads.
  • collectAllocStores cycle protection (visited map) is correct and covered by the new unit tests.

Notes not placed inline:

  • Parallel typechecking removed (perf). The old loader typechecked the import DAG in parallel (loadRecursiveEx goroutines + WaitGroup); the new LoadEx runs packages.Visit(...) sequentially, one package at a time. For large import graphs on multi-core hosts this is a real wall-clock regression, and the tiny hello-world benchmarks in this PR will not surface it. Appears to be a deliberate simplicity/correctness tradeoff — worth confirming it's acceptable.
  • Lost parse cache (perf, minor). The removed loader had a parseCache; typecheckContext.parseFile now does an unconditional os.ReadFile + parser.ParseFile per file. Largely mitigated by the Deduper, but note the loss for repeated in-process loads.
  • Docs. New cl/static_init.go functions (collectAllocStores, handleStoreVal, appendStaticInitPath, staticInitStorePathToAlloc) have non-trivial recursive/cycle/aliasing contracts but no doc comments. Separately, the rewrite deleted the explanatory comment above normalizeEmbedDriverDiagnostics whose rationale still applies to the surviving conditional logic — consider restoring a condensed version. Also confirm the removal of the go-version-skew diagnostic (old runtimeVersion < lpkg.goVersion block) was intentional.

Comment thread internal/packages/load.go
// packages.Load typecheck or parse directly. We request files, imports, embed patterns,
// and module metadata from packages.Load (go list driver), and perform custom parsing
// and typechecking ourselves.
driverCfg.Mode = (origMode &^ (NeedTypes | NeedTypesSizes | NeedTypesInfo | NeedSyntax)) | NeedCompiledGoFiles | NeedImports | NeedName | NeedFiles

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] LoadEx no longer adds NeedDeps; non-NeedDeps callers lose transitive types

driverCfg.Mode derives from origMode and adds NeedImports but never NeedDeps. Per go/packages, with NeedImports but not NeedDeps the Imports map contains ID-only placeholder packages with no CompiledGoFiles/Types, so the subsequent packages.Visit(initial, nil, ...) cannot descend into transitive dependencies and the importerFunc returns "package %q without types was imported from %q" for any real import.

The compiler path (internal/build/build.go:454) sets NeedDeps, so builds are fine. But callers that request types without NeedDeps regress: internal/build/clean.go:57 (loadSyntax | NeedExportFile) and internal/plan9asm/bytealg_sigs_test.go:29 (NeedTypes | NeedImports, package internal/bytealg has imports). The removed refineEx materialized and typechecked the graph off NeedImports alone, so these used to work.

Suggest OR-ing NeedDeps into driverCfg.Mode whenever types/syntax are requested (or documenting the requirement and updating callers). Please confirm the plan9asm test still passes.

Comment thread internal/packages/load.go
hasCompilerSyntaxError := false
for _, err := range lpkg.Errors {
for _, err := range pkg.Errors {
if strings.Contains(err.Msg, "syntax error") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Broad "syntax error" substring match can suppress real diagnostics

This new check flips hasCompilerSyntaxError for any error of any Kind whose message merely contains the substring "syntax error", after which appendError drops all subsequent scanner.ErrorList and types.Error diagnostics. The existing specific check (Kind == ListError && HasPrefix("# ")) already covered the compiler-diagnostic case it was designed for. The broad substring test risks hiding legitimate parse/type errors when an unrelated driver error happens to contain that phrase. Consider dropping the broad check and keeping only the specific one.

if: github.event_name == 'pull_request'
id: merge-base
run: |
git fetch https://github.com/${{ github.event.pull_request.base.repo.full_name }}.git ${{ github.event.pull_request.base.ref }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Untrusted pull_request context interpolated into run: (script injection)

github.event.pull_request.base.ref (and base.repo.full_name, head.sha on the following lines) are interpolated as raw text into a run: shell script. GitHub substitutes context expressions before the shell parses the script, so shell metacharacters in these values execute as code — the classic Actions script-injection pattern. A fork PR can use a branch name containing such characters.

Mitigating factor: the trigger is pull_request (not pull_request_target), so the token is read-only and fork PRs get no secrets, bounding the blast radius to runner code execution / cache poisoning. Still worth fixing by routing the values through env: and quoting them:

env:
  PR_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
  PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
  PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
  git fetch "https://github.com/${PR_BASE_REPO}.git" "$PR_BASE_REF"
  base_sha=$(git merge-base FETCH_HEAD "$PR_HEAD_SHA")

Comment thread ssa/decl.go

// Init initializes the global variable with the given value.
func (g Global) Init(v Expr) {
if g.impl.Name() == moduleZeroName {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Init/InitNil silently no-op for moduleZeroName sentinel

The guard correctly avoids overwriting the shared zero-sized-alloc sentinel's ConstNull initializer, but the early return is silent and matched by name string. Add a brief comment (mirroring the one on NewThreadLocalVar) on both Init and InitNil explaining why the sentinel is skipped, so the dropped write is discoverable by future maintainers.

Comment thread internal/packages/load.go
// package declarations are inconsistent.
lpkg.Types = types.NewPackage(lpkg.PkgPath, lpkg.Name)
lpkg.Fset = ld.Fset
func (tc *typecheckContext) computedSizes(pkg *Package) types.Sizes {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] computedSizes silently substitutes sizes; hardcoded wasm StdSizes

The old LoadExWithGoVersion returned an error when a non-external driver couldn't supply type sizes. computedSizes now always returns non-nil via silent fallbacks, and the wasm fallback changed from types.SizesFor("gc", runtime.GOARCH) to a hardcoded &types.StdSizes{WordSize: 4, MaxAlign: 4}. Silently substituting sizes can mask misconfiguration and, for uncommon arches, produce subtly wrong layout. Consider a comment documenting the intentional fallback and confirming the hardcoded wasm sizes match the targeted ABI.

@cpunion
cpunion force-pushed the codex/benchmark-merge-base-lto branch 2 times, most recently from a994bcb to e573753 Compare August 20, 2026 05:35
@cpunion
cpunion force-pushed the codex/benchmark-merge-base-lto branch from e573753 to bbe6556 Compare August 20, 2026 05:39
@github-actions

Copy link
Copy Markdown

LLGo baseline benchmarks

bbe65568bc0c | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 19288 B 0 B / +0.0% 360.924 ms +9.749 ms / +2.8% (worse) 1.266 ms -12.88 us / -1.0% (better)
Linux cprintf\_lto 19120 B 0 B / +0.0% 346.011 ms -7.38 ms / -2.1% (better) 1.287 ms -25.05 us / -1.9% (better)
Linux fmtprintf 1877440 B +17288 B / +0.9% (worse) 2.953 s +265.3 ms / +9.9% (worse) 3.287 ms +49.8 us / +1.5% (worse)
Linux fmtprintf\_lto 1797016 B +19912 B / +1.1% (worse) 16.022 s +376.2 ms / +2.4% (worse) 3.233 ms -38.72 us / -1.2% (better)
Linux println 68776 B 0 B / +0.0% 357.197 ms +2.769 ms / +0.8% (worse) 1.638 ms -16.93 us / -1.0% (better)
Linux println\_lto 62464 B 0 B / +0.0% 573.550 ms -4.876 ms / -0.8% (better) 1.598 ms +10.09 us / +0.6% (worse)
macOS cprintf 84672 B 0 B / +0.0% 391.875 ms -94.59 ms / -19.4% (better) 2.979 ms -1.043 ms / -25.9% (better)
macOS cprintf\_lto 100912 B 0 B / +0.0% 394.754 ms -36.6 ms / -8.5% (better) 3.196 ms -168.3 us / -5.0% (better)
macOS fmtprintf 1909104 B +16384 B / +0.9% (worse) 2.878 s -335.3 ms / -10.4% (better) 13.759 ms -109.2 us / -0.8% (better)
macOS fmtprintf\_lto 1642928 B +16528 B / +1.0% (worse) 18.965 s -291.6 ms / -1.5% (better) 5.548 ms -914 us / -14.1% (better)
macOS println 121360 B 0 B / +0.0% 373.781 ms -25.27 ms / -6.3% (better) 3.625 ms -651.7 us / -15.2% (better)
macOS println\_lto 128528 B 0 B / +0.0% 548.019 ms -178.9 ms / -24.6% (better) 3.740 ms -208.6 us / -5.3% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.310 ns/op +0.08 ns/op / +0.6% (worse)
Linux BenchmarkMergeCompilerFlags 152.800 ns/op +1 ns/op / +0.7% (worse)
Linux BenchmarkMergeLinkerFlags 95.390 ns/op +0.09 ns/op / +0.1% (worse)
Linux BenchmarkChannelBuffered 35.560 ns/op +1.56 ns/op / +4.6% (worse)
Linux BenchmarkChannelHandoff 28003 ns/op -1315 ns/op / -4.5% (better)
Linux BenchmarkDefer 47.030 ns/op -0.09 ns/op / -0.2% (better)
Linux BenchmarkDirectCall 1.557 ns/op -0.001 ns/op / -0.1% (better)
Linux BenchmarkGlobalRead 1.558 ns/op +0.002 ns/op / +0.1% (worse)
Linux BenchmarkGlobalWrite 2.487 ns/op +0.007 ns/op / +0.3% (worse)
Linux BenchmarkGoroutine 33274 ns/op -12764 ns/op / -27.7% (better)
Linux BenchmarkInterfaceCall 7.816 ns/op -0.286 ns/op / -3.5% (better)
Linux BenchmarkRuntimeGetG 2.181 ns/op -0.003 ns/op / -0.1% (better)
macOS BenchmarkLookupPCRandom 12.310 ns/op -0.4 ns/op / -3.1% (better)
macOS BenchmarkMergeCompilerFlags 120.800 ns/op -6.2 ns/op / -4.9% (better)
macOS BenchmarkMergeLinkerFlags 78.870 ns/op -2.62 ns/op / -3.2% (better)
macOS BenchmarkChannelBuffered 27.070 ns/op +2.62 ns/op / +10.7% (worse)
macOS BenchmarkChannelHandoff 8155 ns/op -1188 ns/op / -12.7% (better)
macOS BenchmarkDefer 43.500 ns/op +7.89 ns/op / +22.2% (worse)
macOS BenchmarkDirectCall 1.187 ns/op +0.138 ns/op / +13.2% (worse)
macOS BenchmarkGlobalRead 1.043 ns/op -0.17 ns/op / -14.0% (better)
macOS BenchmarkGlobalWrite 1.026 ns/op -0.194 ns/op / -15.9% (better)
macOS BenchmarkGoroutine 42363 ns/op -13233 ns/op / -23.8% (better)
macOS BenchmarkInterfaceCall 6.749 ns/op +1.756 ns/op / +35.2% (worse)
macOS BenchmarkRuntimeGetG 2.076 ns/op -0.158 ns/op / -7.1% (better)

Compared with bed9d7c4d4c0 measured in the same runner job.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cpunion

cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #2371 which combines the dependency bump, static init optimization, and the benchmark merge-base/LTO workloads into a unified PR on top of the latest main.

@cpunion cpunion closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant