perf: adopt fat-LTO release profile and a release-fast test profile - #543
Conversation
Release builds ran on cargo's defaults (16 codegen units, no LTO), so the
optimizer never saw across crate boundaries. The hot paths are spread over
several workspace crates and their dependencies -- SSZ encode/decode, hashing,
fork choice traversal -- which is exactly the shape that loses the most to
per-crate codegen units.
Adopt the same release profile ethrex uses: opt-level 3, thin LTO, one codegen
unit.
Clean `cargo build --release --bin ethlambda`, aarch64-apple-darwin, 11 cores,
isolated target dirs:
wall CPU binary
before 315.9s 1714s 30.6 MiB
after 377.8s 1516s 26.6 MiB
+20% -12% -13%
Wall clock grows because a single codegen unit serializes per-crate codegen,
even though total CPU work drops. CI's test job builds in release, so its
cold-cache builds get the same increase.
Runtime effect is not measured here: the repo has no benchmarks, and the
meaningful comparison (block build and aggregation latency) needs a devnet A/B.
🤖 Claude Code ReviewThis is a small, self-contained build-configuration change. Full review below. Summary
ReviewCorrectness: No functional risk — this is a compiler/linker configuration change only. Consensus-layer impact: None. No logic in fork choice, state transition, attestation processing, or signature handling is touched. Performance: Reasonable trade-off and well-justified — cross-crate inlining plausibly helps here since the hot paths (SSZ encode/decode,
CI/build implications to double check (not visible in this diff, so flagging for the reviewer to verify, not a code issue):
Rust idiom: Profile section placement and comment are clear and appropriately explain the why (cross-crate inlining rationale) rather than just restating the what. No issues. VerdictLow-risk, well-documented change with clear rationale and honest trade-off disclosure. No correctness, security, or consensus-logic concerns. Only suggestion is to follow up with the mentioned devnet A/B to confirm the runtime win materializes, and to confirm CI time/memory budgets tolerate the slower cold build. Automated review by Claude (Anthropic) · sonnet · custom prompt |
Greptile SummaryConfigures optimized release builds in the workspace root.
Confidence Score: 5/5The PR appears safe to merge with no actionable correctness or security issues identified. The changed Cargo release-profile values are valid, preserve the existing release-mode correctness defaults, and affect optimization and build performance rather than application contracts.
|
| Filename | Overview |
|---|---|
| Cargo.toml | Adds valid workspace-wide Cargo release-profile settings for Thin LTO and single-unit code generation; no correctness issue identified. |
Reviews (1): Last reviewed commit: "perf: enable thin LTO and single codegen..." | Re-trigger Greptile
🤖 Kimi Code ReviewCargo.toml (lines 29-37) This change is appropriate for a consensus client where runtime performance directly affects network viability. The configuration correctly prioritizes cross-crate inlining for SSZ serialization and fork-choice hot paths. Observations:
Recommendation: [profile.release-with-debug]
inherits = "release"
debug = 1 # Line tables only, minimal binary size impact
strip = falseVerdict: No blocking issues. The change is correct and appropriate for the domain. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
No correctness, safety, or consensus-layer findings in the actual client logic: the PR only changes Cargo release-profile settings and does not touch fork choice, attestation handling, state transition, XMSS, or SSZ code. I couldn’t run Cargo verification in this environment because Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
`make test` ran on `--release`, which after the previous commit means thin LTO and a single codegen unit, so every test rebuild re-links the whole program. Tests do need release-grade opt-level -- signature verification/aggregation stack-overflows without it, which is why they were on `--release` at all -- but they do not need a whole-program-optimized binary. Add ethrex's `release-fast` profile (release opt-level, no LTO, parallel codegen, line-tables-only debuginfo, incremental) and point `make test` at it. Rebuild after a one-file source edit, `cargo test --workspace --no-run`: --release 88.0 s --profile release-fast 4.6 s Costs: the cold build of the new profile dir is 461.9 s, and `target/release-fast` holds ~5.3 GiB against `target/release`'s 1.6 GiB, mostly incremental artifacts. In CI the incremental flag is inert: the test job's `Swatinem/rust-cache` sets `CARGO_INCREMENTAL=0`, which overrides the profile (verified -- no `-C incremental=` reaches rustc under that env var). CI's win is dropping LTO and restoring parallel codegen.
|
Hey @MegaRedHand , I think lto set to fat has the best runtime improvement |
Measured on the 8-node eth-4 devnet: two images built from the same commit,
differing only in [profile.release], running simultaneously on 4 nodes each for a
30 min window, so both arms saw identical chain conditions. Aggregator node
excluded (its extra load is not part of what is being compared).
thin fat
state transition 9.027ms 8.836ms -2.12% (7.1 sigma)
block build 946.6ms 937.4ms -0.98% (3.6 sigma)
payload aggregation 8.832ms 8.728ms -1.18%
container CPU 0.444 0.440 -0.91%
Every fat node beat every thin node on both state transition and block build --
no overlap between the two groups of four:
state transition thin: 8.997 9.026 9.036 9.018 fat: 8.791 8.822 8.855 8.875
block build thin: 945.1 946.1 943.9 950.0 fat: 939.0 936.4 938.8 935.3
Block processing and attestation processing come out even between thin and fat
(+1.6% and +1.2%, both inside noise): the default -> LTO step already captures
the cross-crate inlining win there, and fat's extra gain shows up in total state
transition time instead.
Cost: docker build 284s for fat against 290s for thin, image 205MB against 209MB,
so no measurable penalty on a 16-core host. Note this is a single measurement per
arm; fat LTO's peak link memory is untested on 16GB CI runners, though CI never
builds this profile (cargo check for lint, release-fast for tests).
This diverges from ethrex, which uses thin.
🗒️ Description / Motivation
We had no
[profile]section at all, so release builds ran on cargo's defaults:16 codegen units, no LTO. The optimizer never inlined across crate
boundaries, and our hot paths are spread across workspace crates and their
dependencies (SSZ encode/decode,
hash_tree_root, fork choice traversal) — theshape that loses most to per-crate codegen units.
Two deliberate divergences from ethrex,
whose profiles this started as a copy of:
lto = "fat", not thin. Measured, see below.release-fastruns the whole test suite, where ethrex applies it only toits
ef_testsMakefiles and runs everything else on the dev profile. Wecan't: signature verification/aggregation stack-overflows without
release-grade opt-level, which is why
make testused--releaseto beginwith.
ethrex's other profiles are not ported:
[profile.dev] debug = 2is alreadycargo's default, and
release-with-debug/release-with-debug-assertionshaveno user here.
What Changed
Cargo.toml[profile.release](fat LTO, 1 CGU) and[profile.release-fast]Makefiletesttarget:--release→--profile release-fastCLAUDE.md.github/PULL_REQUEST_TEMPLATE.mdmake testNo source changes.
Correctness / Behavior Guarantees
debug-assertions/overflow-checksstay at release defaults (off) in bothprofiles, so tests keep the assertion semantics they had under
--release.release-fastinheritsopt-level = 3, the property the stack-overflowavoidance depends on.
eth-4 devnet for over 30 minutes, finalizing normally, heads in sync.
Measured: latency, on an 8-node devnet
Method: images built from the same commit differing only in
[profile.release],then run simultaneously on 4 nodes each so both arms see identical chain
conditions (a sequential before/after would confound the profile with chain
load — an earlier crossover attempt of mine did exactly that and had to be
thrown out). Aggregator node excluded; its extra load isn't what's being
compared. Negative = lower latency.
The two LTO steps buy different things:
attestation processing −17%. Total state transition barely moves, since those
sub-millisecond stages are a small slice of it.
while block processing and attestation processing land slightly the wrong way
(+1.6%, +1.2%) — both inside noise at 0.2σ and 0.5σ.
On the fat-vs-thin window, every fat node beat every thin node, no overlap:
Confidence differs sharply by leg, so both are worth stating:
fat vs defaultis composed from the two 30-minute measurements, not measureddirectly. A direct 5-minute fat-vs-default window gave larger numbers
(−5.30% / −21.40% / −19.10%) but its baselines ran 1–5% high, so I trust the
composed column and report the short one only as a sanity check.
Heavy crypto specifically
leanVM's XMSS aggregation benchmark at the rev we pin, its profiles replaced by
ours, paired over 5 alternating rounds on a quiesced eth-4 (devnet paused):
−0.52%, significant (all 5 rounds agree). Small, as expected: that code is
intra-crate, heavily monomorphized numeric work with little cross-crate inlining
for LTO to find. It rules out a regression on the proving path, which is what it
was run for.
Measured: build cost
Whole-workspace binary, clean builds, isolated
CARGO_TARGET_DIR, 11-coreM-series (thin measured; the default→LTO delta is the point):
Wall grows because one codegen unit serializes per-crate codegen even as total
CPU drops. Docker builds on the 16-core eth-4 host: fat 284 s / 205 MB against
thin 290 s / 209 MB, i.e. fat costs nothing measurable relative to thin.
Test builds, which is why
release-fastexists — rebuild after one real one-fileedit,
cargo test --workspace --no-run, both profile dirs pre-warmed:--release--profile release-fastWithout
release-fast, this PR would make every test rebuild pay a full LTOrelink. Costs: cold build of that profile dir is 461.9 s and
target/release-fastholds ~5.3 GiB against
target/release's 1.6 GiB, so local disk grows for anyonewho both builds
--releaseand runs tests.CI note: the test job's
Swatinem/rust-cachesetsCARGO_INCREMENTAL=0,which overrides
incremental = true(verified — no-C incremental=reachesrustc under that env var, and the action strips incremental artifacts before
caching). In CI the
release-fastwin islto = falseplus parallel codegen.Risks / open items
this profile today (
cargo checkfor lint,release-fastfor tests), but theDocker publish workflow does.
...thin-ltofrom before the fat switch; the profile isfat.
Tests Added / Run
No tests added (build-configuration change).
make fmt— cleanmake lint(cargo clippy --workspace --all-targets -- -D warnings) — cleanmake testunderrelease-fast— all passing: forkchoice spectests 122, stfspectests 46, ssz spectests 119, signature spectests 3, plus all unit tests;
0 failures. Also green under
--releasebefore the profile switch.docker buildon eth-4 and by running theresulting image on 8 devnet nodes for 30+ minutes.
Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(cargo test --workspace --profile release-fast) — all passing