Skip to content

ssa: lower large array comparisons to runtime equality algorithms - #2416

Open
cpunion wants to merge 7 commits into
xgo-dev:mainfrom
cpunion:codex/array-equality-runtime-20260826
Open

ssa: lower large array comparisons to runtime equality algorithms#2416
cpunion wants to merge 7 commits into
xgo-dev:mainfrom
cpunion:codex/array-equality-runtime-20260826

Conversation

@cpunion

@cpunion cpunion commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

LLGo currently expands every array equality operation element by element. The Go compiler only inlines small, simple arrays and uses type equality algorithms for larger values. For GOROOT/test/fixedbugs/bug449.go, LLGo's expansion creates more than a million comparisons and causes LLVM to exhaust the runner resource budget.

This PR:

  • keeps small scalar arrays inline, following the Go compiler's comparison heuristic;
  • lowers regular-memory arrays to runtime.memequal and other comparable arrays to runtime.arrayequal;
  • reuses backing storage only for non-escaping local arrays proven unchanged after the SSA load, while retaining a snapshot temporary for mutable or non-addressable operands;
  • omits dead whole-array loads when every executable use is a non-inline comparison with two proven-stable addresses;
  • derives the GOROOT runner's temporary GOPATH link from the repository's current module path instead of the pre-migration github.com/goplus/llgo path;
  • removes the four Darwin/Linux resource expectations for fixedbugs/bug449.go.

The runtime algorithm also restores Go's required left-to-right short-circuit behavior for arrays containing interfaces; the previous eager element expansion could compare a later uncomparable dynamic value and panic after an earlier mismatch.

Measured result

Increasing the Windows test environment to 7 GiB was not sufficient: the old lowering still timed out after 10 minutes, with about 6.2 GiB sampled RSS and heavy paging. Before the final frontend load-elision optimization, runtime lowering alone already made the official Go 1.26.5 case pass without increasing the runner limit:

Host LLGo build Peak process-group RSS Result
macOS/arm64 15.989 s 1679.1 MiB pass
Linux/amd64 (OrbStack) 21.802 s 1488.2 MiB pass

The Linux timing includes amd64 translation on an Apple Silicon host and is recorded as a functional/resource check, not a host-to-host speed comparison.

Why the frontend reuses proven-stable addresses

Array size is sufficient to choose between inline comparison and a runtime equality helper, and CanInlineArrayEqual already provides that gate. It is not sufficient to decide whether the runtime helper may read an operand's original address: a loaded array is a value snapshot, and the source allocation may be modified before the comparison.

An A/B run of the same Go 1.26.5 fixedbugs/bug449.go case shows why always materializing runtime operands into temporary arrays is not viable:

Runtime operand strategy LLGo generated build Peak process-group RSS Result
Reuse non-escaping local storage proven unchanged 8.982 s 1669.0 MiB pass
Always copy values to temporary arrays 5.266 s before termination 4146.5 MiB failed 4096 MiB guard

The address-use walk is therefore deliberately conservative: it reuses storage only when all stores precede the SSA load and falls back to a snapshot temporary for heap, aliased, mutated, cross-block, or otherwise unknown operands. This avoids LLVM's aggregate-copy memory growth without changing Go value semantics.

Avoiding dead aggregate loads before LLVM

Once both operands have proven-stable addresses, the runtime equality helper does not consume their SSA aggregate values. The frontend previously emitted those whole-array loads anyway because it compiled each SSA load before reaching the comparison. LLVM eventually removed the dead loads, but constructing and optimizing them still dominated this generated case.

The final optimization omits a load only when every executable user is a non-inline array equality/inequality operation and both operands of every such comparison have reusable local addresses. Three alternating runs of prebuilt baseline and optimized compilers on the same macOS/arm64 Go 1.26.5 case produced:

Frontend lowering Median LLGo build Median peak process-group RSS Samples
Runtime address reuse, dead aggregate loads retained 8.740 s 1669.4 MiB 3
Runtime address reuse, dead aggregate loads omitted 3.328 s 250.9 MiB 3

This reduces the median build time by about 62% and peak RSS by about 85% on this stress case. The optimized Go 1.25.0 run also passed (4.277 s, 244.2 MiB). Mutable snapshots, small inline comparisons, heap values, parameters, and unknown uses keep their existing value-preserving lowering.

Why fixedbugs/bug449.go stresses time and memory

The measurements above come from this generator in Go's fixedbugs/bug449.go. It deliberately emits 1,024 progressively larger array-equality tests:

const ntest = 1024

func main() {
	var decls, calls bytes.Buffer

	for i := 1; i <= ntest; i++ {
		s := strconv.Itoa(i)
		decls.WriteString(strings.Replace(decl, "$", s, -1))
		calls.WriteString(strings.Replace("call(test$)\n\t", "$", s, -1))
	}

	program = strings.Replace(program, "$DECLS", decls.String(), 1)
	program = strings.Replace(program, "$CALLS", calls.String(), 1)
	fmt.Print(program)
}

const decl = `
type T$ [$]uint8
func test$() bool {
	v := T${1}
	return v == [$]uint8{2} || v != [$]uint8{1}
}`

Each iteration substitutes i for $, generating a distinct array type [i]uint8 and its equality/inequality function. With 1,024 such functions, expanding large array comparisons makes compilation consume excessive time and memory. This change keeps small comparisons inline but lowers large arrays through the runtime equality path, which produces the measured reduction.

Validation

  • Go 1.25.0 and Go 1.26.5: go test ./ssa ./cl -run 'Test(ArrayEqualLowering|ArrayCompareReusesImmutableLocalStorage|CanInlineArrayEqual)$' -count=1
  • Go 1.25.0 and Go 1.26.5 LLGo execution of TestLargeArrayEquality, TestArrayEqualityPreservesLoadedValue, and TestArrayEqualityUsesElementSemantics
  • cl/_testgo/equal and cl/_testrt/slice2array lowering/execution tests
  • Go 1.25.0 and Go 1.26.5 GOROOT fixedbugs/bug449.go on macOS/arm64; Go 1.26.5 on Linux/amd64
  • full go test ./ssa -count=1
  • go test ./test/goroot -count=1

The full local cl suite reaches an unrelated existing LLVM 19 warning in TestRunAndTestFromTestlto/abitype_runtime (+zcm/+zcz target features); all changed and array-specific cl tests pass.

@cpunion cpunion added the go-test-compat Go standard-library and GOROOT test compatibility label Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

78a9f63bc408 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Text size vs base Build vs base Run vs base
Linux cprintf 19256 B 0 B / +0.0% 387 B 0 B / +0.0% 341.433 ms -10.77 ms / -3.1% (better) 1.259 ms -47.93 us / -3.7% (better)
Linux cprintf-lto 19088 B 0 B / +0.0% 368 B 0 B / +0.0% 355.276 ms +15.49 ms / +4.6% (worse) 1.386 ms +75.98 us / +5.8% (worse)
Linux fmtprintf 1653232 B +8 B / +0.0004839% (worse) 501083 B 0 B / +0.0% 2.959 s +92.3 ms / +3.2% (worse) 3.337 ms -19.8 us / -0.6% (better)
Linux fmtprintf-lto 1525824 B 0 B / +0.0% 459700 B 0 B / +0.0% 9.072 s -26.52 ms / -0.3% (better) 3.202 ms +115.6 us / +3.7% (worse)
Linux println 62032 B 0 B / +0.0% 15261 B 0 B / +0.0% 358.175 ms +16 ms / +4.7% (worse) 1.653 ms +52.96 us / +3.3% (worse)
Linux println-lto 53840 B 0 B / +0.0% 12882 B 0 B / +0.0% 543.380 ms +21.09 ms / +4.0% (worse) 1.604 ms +24.42 us / +1.5% (worse)
macOS cprintf 84480 B 0 B / +0.0% 16493 B 0 B / +0.0% 349.748 ms -120.3 ms / -25.6% (better) 3.135 ms +149.9 us / +5.0% (worse)
macOS cprintf-lto 100704 B 0 B / +0.0% 16473 B 0 B / +0.0% 337.038 ms -236.9 ms / -41.3% (better) 2.389 ms -767.2 us / -24.3% (better)
macOS fmtprintf 1498256 B 0 B / +0.0% 882133 B 0 B / +0.0% 2.126 s -413.5 ms / -16.3% (better) 4.722 ms -1.058 ms / -18.3% (better)
macOS fmtprintf-lto 1208976 B 0 B / +0.0% 875129 B 0 B / +0.0% 5.918 s +344.6 ms / +6.2% (worse) 8.280 ms +3.564 ms / +75.6% (worse)
macOS println 114832 B 0 B / +0.0% 34849 B 0 B / +0.0% 322.254 ms -57.66 ms / -15.2% (better) 2.776 ms -1.252 ms / -31.1% (better)
macOS println-lto 118656 B 0 B / +0.0% 32489 B 0 B / +0.0% 423.155 ms -62.83 ms / -12.9% (better) 2.786 ms -2.221 ms / -44.4% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.270 ns/op 0 ns/op / +0.0%
Linux BenchmarkMergeCompilerFlags 150.700 ns/op -2.1 ns/op / -1.4% (better)
Linux BenchmarkMergeLinkerFlags 94.770 ns/op -0.1 ns/op / -0.1% (better)
Linux BenchmarkChannelBuffered 35.830 ns/op +0.3 ns/op / +0.8% (worse)
Linux BenchmarkChannelHandoff 27697 ns/op +663 ns/op / +2.5% (worse)
Linux BenchmarkDefer 49.510 ns/op -0.87 ns/op / -1.7% (better)
Linux BenchmarkDirectCall 1.557 ns/op +0.001 ns/op / +0.1% (worse)
Linux BenchmarkGlobalRead 1.869 ns/op +0.001 ns/op / +0.1% (worse)
Linux BenchmarkGlobalWrite 2.480 ns/op -0.003 ns/op / -0.1% (better)
Linux BenchmarkGoroutine 31641 ns/op -1730 ns/op / -5.2% (better)
Linux BenchmarkInterfaceCall 7.788 ns/op -0.617 ns/op / -7.3% (better)
Linux BenchmarkRuntimeGetG 2.179 ns/op -0.001 ns/op / -0.04587% (better)
macOS BenchmarkLookupPCRandom 11.060 ns/op -0.81 ns/op / -6.8% (better)
macOS BenchmarkMergeCompilerFlags 108.400 ns/op +13.1 ns/op / +13.7% (worse)
macOS BenchmarkMergeLinkerFlags 72.530 ns/op -0.11 ns/op / -0.2% (better)
macOS BenchmarkChannelBuffered 24.070 ns/op +1.04 ns/op / +4.5% (worse)
macOS BenchmarkChannelHandoff 8111 ns/op +1305 ns/op / +19.2% (worse)
macOS BenchmarkDefer 35.960 ns/op -1.46 ns/op / -3.9% (better)
macOS BenchmarkDirectCall 1.173 ns/op +0.162 ns/op / +16.0% (worse)
macOS BenchmarkGlobalRead 1.309 ns/op +0.305 ns/op / +30.4% (worse)
macOS BenchmarkGlobalWrite 1.267 ns/op +0.208 ns/op / +19.6% (worse)
macOS BenchmarkGoroutine 28834 ns/op +531 ns/op / +1.9% (worse)
macOS BenchmarkInterfaceCall 4.769 ns/op -0.332 ns/op / -6.5% (better)
macOS BenchmarkRuntimeGetG 2.199 ns/op -0.111 ns/op / -4.8% (better)

Compared with 6fe8babd468c measured in the same runner job.

@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: runtime-based array equality lowering

Solid change overall. Routing large-array comparisons through memequal/arrayequal (instead of scalarizing every array via extractvalue) and reusing immutable local backing storage to avoid a copy is a clear net win. The immutableLocalArrayLoadAddr analysis is sound: it is conservative in the correct direction — any store not provably preceding the load (including all cross-block stores), any pointer escape, and any non-derivation referrer all reject the reuse, so a reused address always reflects the loaded value. The snapshot/immutable tests confirm the mutation-after-load case falls back to the value-preserving temporary. callArrayEqual's StackSave/StackRestore pairing and the memequal size (SizeOf(x.Type)) are correct.

Two refinements below; nothing blocking.

Minor / non-blocking notes not placed inline:

  • cl/compile.go:1792 instructionPrecedes re-scans block.Instrs from the head on each call (O(k × block length) for an alloc with k stores). Negligible for typical array allocs (k=1), but it could track an ordering index if this ever shows up in hot compiles.
  • The block-local store-ordering check means an array whose alloc has stores in a different (even dominating) block bails to a full copy on the runtime path. Safe, but a missed optimization for some control-flow shapes.

Comment thread ssa/expr.go Outdated
Comment thread cl/compile.go Outdated
@cpunion
cpunion requested review from visualfc and xushiwei August 26, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go-test-compat Go standard-library and GOROOT test compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant