perf(core/vm): flag-gated shared precompile result caches for serial import + block builder, widen keccak cache - #2318
Open
lucca30 wants to merge 9 commits into
Open
perf(core/vm): flag-gated shared precompile result caches for serial import + block builder, widen keccak cache#2318lucca30 wants to merge 9 commits into
lucca30 wants to merge 9 commits into
Conversation
…recompileCache flag (default off, no behavior change)
…nablePrecompileCache (length-aware key, preimage-on-hit)
…; guard against hit-slice aliasing
…cessor behind EnablePrecompileCache
…and sealing EVM behind EnablePrecompileCache
… meters out of the store lock, rename bytes→bytes_total counter, assert flag in ApplyTo test
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #2318 +/- ##
===========================================
+ Coverage 54.07% 54.11% +0.03%
===========================================
Files 907 908 +1
Lines 162012 162136 +124
===========================================
+ Hits 87615 87740 +125
+ Misses 68984 68982 -2
- Partials 5413 5414 +1
... and 23 files with indirect coverage changes
🚀 New features to boost your workflow:
|
Fixes the lint and diffguard findings from PR CI without changing any consensus behavior: - lint (unconvert): drop the redundant common.Hash() conversion in the widened keccak store write (hasherBuf is already common.Hash). - lint (goimports): reformat the shared_cache.go metric var block. - complexity: extract opKeccak256's variable-length cache branch into keccakWidenedHit, bringing opKeccak256 back under the cognitive-complexity threshold. Behavior is byte-identical (hit path returns; miss path computes into hasherBuf and falls through, exactly as before). - function size: extract commitWork's build-cache construction into newBuildVMCaches, bringing commitWork back under the size threshold. Test coverage closing mutation-testing gaps: - TestCacheableKeccakLen pins both eligibility boundaries (n>0 and n<=8192). - TestNewBuildVMCaches asserts the build path gets no caches when the flag is off and the extended (widened-keccak) cache set when on. - TestPrecompileCacheFlagPlumbing pins the flag default (off) and its propagation through buildEth into the eth/VM config.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wires bor's existing per-block shared VM result caches (ECRECOVER + KECCAK256) into the two execution paths that currently lack them — the serial import processor and the block builder (
miner/worker.go) — and widens the 64-byte-only keccak cache to cover the small variable-length keccak inputs that dominate Polygon's uncached VM compute time. All new behavior is behind a default-off flag (--precompile-cache); with the flag off, block validation and production are byte-for-byte unchanged.Everything is content-keyed (never tx-index/order-keyed), which is what makes it correct on the build path where mempool order ≠ sealed order, and the caches are per-block / per-building-cycle (matching the existing
sharedBlockCachespattern), so fork-safety is free and there is no cross-block staleness.Why (measured scope)
We instrumented a mainnet copy-node (content-keyed observer, never returns cached results, cannot affect consensus) and measured per-op call count × per-call time over a sampled window of mainnet blocks. Ranked by total observed time, KECCAK256 + ECRECOVER are the dominant cacheable VM compute — and both are already cached in production, but only for the import prefetch↔parallel paths, not for serial import or the builder. KECCAK256 non-64B inputs (avg ~88 B, clustered ≤ ~350 B) are ~60k calls/block and not cached at all today.
Caveats stated honestly: this is a single-window sample, not a multi-day census; the observer's raw totals include the losing racing processor (serial vs parallel), so absolute numbers are directional and should be re-confirmed with a cache-on-vs-off pprof/wall-clock cross-check. MODEXP/BN256/BLS/KZG/blake2F are fully reachable (they live in the active precompile sets) but were called ~never in-sample, so a geth-style cross-block LRU for them is not the first-dollar investment — see Future work. This is an MVP prioritization, not a proof the broader set never matters.
How
core/vm/shared_cache.go), backing store chosen by a committed-benchmemmicrobench (shardedmap[string]common.Hash+RWMutex: 12.5 ns/op, 0 B/0 allocs on Load, beatingsync.Map-string and fixed-buckets). Per-block entry cap (stop-inserting-when-full) bounds adversarial memory.vm.SharedResultCachesowner replacing the unexportedcore.sharedBlockCaches, so bothcoreandminercan construct it.ApplyToalways wires the legacy jumpDest/keccak-64B/ecrecover caches (preserving today's import prefetch↔parallel sharing regardless of the flag); it wires the widened store + sets the flag only when extended.opKeccak256: variable-length inputs hit the widened store behind the flag. Length-aware key (a short input padded to a wider size must never alias —keccak(x) ≠ keccak(x‖0x00)); value stored ascommon.Hashby value; preimage recorded on hit for every size; gas unchanged (charged by the interpreter before the opcode; a hit skips only computation).genParamsbefore the prefetch goroutine launches, shared by the build prefetcher and the sealing EVM. Only those two EVMs are wired; pre-tx/system EVMs,FinalizeAndAssemble, and Bor state-sync are intentionally excluded (documented).SetPrecompiles), plus a defensive clone against cached-output aliasing.vm/cache/keccak/{hit,miss,entries,bytes_total,lock_wait},vm/cache/ecrecover/{hit,miss}(distinct from the state-readerchain/cache/*meters).Testing
-race), ecrecover override-exclusion + hit/miss no-alias, fork-boundary (crosses a real hardfork gate), fuzz (widened cache vs direct keccak), and-raceon concurrent prefetch+seal sharing one cache set.Result — overnight A/B on a mainnet copy-node
Validated on a mainnet copy-node in producer mode over a 12h interleaved A/B (24 alternating
30-min rounds, 12 per arm, ~26k blocks). Arm A = flag off, arm B = flag on; only
--precompile-cachediffers between arms.--precompile-cachegives a ~8.2% block-build throughput win (build-loop MGas/s, measuredfrom the
Commit new sealing worklog line'sgas=/elapsed=fields). The effect is robust:every arm-B round beat every arm-A round (no overlap; A 244.8 ± 6.4 vs B 264.8 ± 7.3 MGas/s on a
round-mean basis). It is not a block-size artifact — recomputed at matched block size across
fine gas bands, arm B wins in 10/10 bands (+6.4% to +11.7%), with the gap widening on larger
blocks, consistent with a genuine per-tx cache benefit.
This is a CPU-latency optimization on the build path, not a protocol-ceiling change: it does not
alter gas limits or block time, and translates to producer throughput only insofar as block
production is build-time-bound. Per-EVM hit attribution is not wired, so the win is not decomposed
into cross-EVM prefetcher→sealer sharing vs. intra-sealer per-block caching — both are real and
both are enabled by this flag.
Full A/B methodology, per-round data, and the measurement harness are retained internally (PoS
block-production instrumentation campaign) and available to reviewers on request.
Rollout
Flag off by default. Deploy flag-off first (verify no-op), then enable on a non-producer node for the import-path changes, and only then on a dedicated producer canary for the build-path cache (the build commit is isolated precisely because a cache bug there makes a validator produce an invalid block). Watch
vm/cache/*, block-execution timers,lock_wait, and producer block validity.Future work (tracked, not built here)
CallObserver(not shipped here) remains the trigger to re-measure if traffic shifts.meaningful share of build-time CPU is ordinary EVM bytecode re-executed with identical inputs by
the busiest contracts on the chain (hot
view/purepaths, fixed-point/math libraries,signature- and Merkle-proof verifiers written in Solidity). A content-keyed per-block result
cache keyed on
(code identity, input)over a curated allowlist of such hot code segments couldextend the same win into pure-EVM territory. Prerequisites: (a) a profiling pass to rank
high-recurrence pure snippets per contract from real traffic; (b) a soundness argument that each
cached region is a genuine pure function of its inputs — no
SLOAD/SSTORE, externalCALL, orenvironment/opcode state dependence — before a hit may substitute for execution; (c) gas charging
left completely untouched (a hit skips only computation, never metering). Speculative and
unmeasured — a direction, not a committed design.