Skip to content

perf: CSR-flatten L/U factors for cache locality and fewer allocs#24

Open
andig wants to merge 2 commits into
mainfrom
perf/csr-flatten-lu
Open

perf: CSR-flatten L/U factors for cache locality and fewer allocs#24
andig wants to merge 2 commits into
mainfrom
perf/csr-flatten-lu

Conversation

@andig

@andig andig commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Both factorizations stored the L-file (and U, for the default sparse LU) as an array-of-slices with per-step append growth — one heap allocation per elimination step, and pointer-chased, scattered memory in the solve loops. This flattens the immutable parts into CSR-style contiguous arrays keyed by a per-step offset table.

Changes

simplex/ft.go — Forrest-Tomlin L-file → lFR/lFV/lPtr
L is immutable between refactorizations (replaceColumn never touches it), so flattening is safe. forwardLR/btran now stream contiguous memory; clone copies 3 flat arrays instead of 2×m sub-slices.

  • clone: −32% time (112µs → 76µs), −597 allocs (1460 → 863) at m=300.
  • per-pivot solve ~2.5% faster (locality).

simplex/sparselu.go — default LU L and U → lFlat*/uFlat*/lPtr/uPtr
sparseLU is immutable after build (eta updates are external; these fields are read only within this file), so solve/solveT just stream the flat arrays.

  • luFactorize allocations on case 020: 1.72M → 337k (5.1× fewer).
  • total solve allocations on 020: 2.47M → 1.22M (halved).

Performance note

Case 020 wall-clock is unchanged (~7.3s) — it is compute-bound on the LU arithmetic, not GC-bound. The win is the halved allocation count → lower GC pressure and pause jitter on the serving path (madvise CPU 0.40s → 0.30s).

Benchmarks added

  • BenchmarkLUFactorizeSynth — self-contained synthetic kernel (factor + solve + solveT); guards the per-factorize allocation count without external gob data.
  • BenchmarkFTReplaceReal / BenchmarkFTClone — engine-cadence FT costs. BenchmarkFTReplaceReal refactorizes every maxEtas like the engine, unlike the existing BenchmarkFTReplace which lets the R-file grow unbounded (unrepresentative 41× slower).

All golden cases and simplex tests pass.

🤖 Generated with Claude Code

andig and others added 2 commits July 15, 2026 21:33
Both the Forrest-Tomlin factor (ft.go) and the default sparse LU
(sparselu.go) stored L (and U, for sparseLU) as array-of-slices with
per-step append growth: one heap allocation per elimination step, and
pointer-chased, scattered memory in the solve loops.

Flatten the immutable parts into CSR-style contiguous arrays keyed by a
per-step offset table:

- ft.go: L-file -> lFR/lFV/lPtr. L is immutable between refactorizations
  (replaceColumn never touches it), so this is safe. forwardLR/btran now
  stream contiguous; clone copies 3 flat arrays instead of 2*m sub-slices.
  clone -32% time (112us->76us), -597 allocs (1460->863) at m=300.

- sparselu.go: L and U -> lFlat*/uFlat*/lPtr/uPtr. sparseLU is immutable
  after build (eta updates are external; fields read only in this file),
  so solve/solveT just stream flat. luFactorize allocations on case 020:
  1.72M -> 337k (5.1x fewer); total solve allocations 2.47M -> 1.22M.

Case 020 wall-clock is unchanged (~7.3s): it is compute-bound on the LU
arithmetic, not GC-bound. The win is halved allocation count -> lower GC
pressure and pause jitter on the serving path. All golden cases and
simplex tests pass.

Benchmarks: add BenchmarkLUFactorizeSynth (self-contained synthetic
kernel; guards the per-factorize alloc count without external gob data)
and BenchmarkFTReplaceReal/BenchmarkFTClone (engine-cadence FT costs,
unlike BenchmarkFTReplace which lets the R-file grow unbounded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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