Skip to content

execution/cache: give growLRU the same atomic entry count - #23523

Merged
AskAlexSharov merged 1 commit into
alex/cache_len_contention_37from
alex/codecache_len_contention_37
Aug 24, 2026
Merged

execution/cache: give growLRU the same atomic entry count#23523
AskAlexSharov merged 1 commit into
alex/cache_len_contention_37from
alex/codecache_len_contention_37

Conversation

@AskAlexSharov

Copy link
Copy Markdown
Collaborator

Stacked on #23522 — review that first; this PR's own diff is the second commit.

growLRU.Add ran its grow check through freelru.ShardedLRU.Len, which RLocks every shard in turn, on every add while the LRU was below its ceiling; maybeGrow scanned again inside resizeMu. CodeCache drives three of these layers (hashToCode, codeHashToCode, codeSizeByCodeHash) straight off the EVM code path.

Same fix as #23522: reuse lruGen, generalised over the value type, so the count comes from an atomic maintained by add and the OnEvict callback. Len() becomes O(1) too. putContentLocked already removes an existing entry before re-adding, so every Add reaching freelru is for an absent key — the invariant the counter needs.

BenchmarkGrowLRUParallelAddGrow (added), M4 Max, -cpu 10:

                       │ growLRU_Len  │           growLRU_counter           │
                       │    sec/op    │    sec/op     vs base               │
GrowLRUParallelAddGrow   756.90n ± 8%   96.58n ± 13%  -87.24% (p=0.002 n=6)

CodeCache.Put end-to-end does not move (500.2n → 497.1n, p=0.394): addrToHash / addrToCodeHash are hashicorp/golang-lru caches behind a single global mutex, and that dominates the put path. This PR removes real CPU time from the content layers; unlocking it end-to-end needs that addr LRU sharded, which is separate work.

Two tests pin counter == the LRU's real length: growLRU directly across add / remove / eviction / grow / Purge, and all three CodeCache layers under concurrent puts. Both verified to fail against a mutated counter.

No TDD cycle: performance refactor with no intended behaviour change.

https://claude.ai/code/session_01WFkAYPPhqPe1NXg41Nph78

growLRU.Add ran its grow check through freelru's ShardedLRU.Len, which RLocks
every shard in turn, on every add while the LRU was below its ceiling.
maybeGrow scanned again inside resizeMu. CodeCache drives three of these
layers (hashToCode, codeHashToCode, codeSizeByCodeHash) from the EVM code path.

Reuse GenericCache's lruGen, generalised over the value type, so the count
comes from an atomic maintained by add and the OnEvict callback. Len() becomes
O(1) too. putContentLocked already removes an existing entry before re-adding,
so every Add reaching freelru is for an absent key.

BenchmarkGrowLRUParallelAddGrow, M4 Max, -cpu 10: 756.9n -> 96.6n (-87.2%,
p=0.002 n=6). CodeCache.Put end-to-end does not move: addrToHash is a
hashicorp lru.Cache behind one global mutex, which dominates that path.

Claude-Session: https://claude.ai/code/session_01WFkAYPPhqPe1NXg41Nph78
@AskAlexSharov
AskAlexSharov marked this pull request as ready for review August 24, 2026 02:19
@AskAlexSharov
AskAlexSharov merged commit d6690ac into alex/cache_len_contention_37 Aug 24, 2026
248 checks passed
@AskAlexSharov
AskAlexSharov deleted the alex/codecache_len_contention_37 branch August 24, 2026 06:30
AskAlexSharov added a commit that referenced this pull request Aug 24, 2026
Stacked on #23522 — review that first; this PR's own diff is the second
commit.

`growLRU.Add` ran its grow check through `freelru.ShardedLRU.Len`, which
RLocks every shard in turn, on **every add** while the LRU was below its
ceiling; `maybeGrow` scanned again inside `resizeMu`. `CodeCache` drives
three of these layers (`hashToCode`, `codeHashToCode`,
`codeSizeByCodeHash`) straight off the EVM code path.

Same fix as #23522: reuse `lruGen`, generalised over the value type, so
the count comes from an atomic maintained by `add` and the `OnEvict`
callback. `Len()` becomes O(1) too. `putContentLocked` already removes
an existing entry before re-adding, so every `Add` reaching freelru is
for an absent key — the invariant the counter needs.

`BenchmarkGrowLRUParallelAddGrow` (added), M4 Max, `-cpu 10`:

```
                       │ growLRU_Len  │           growLRU_counter           │
                       │    sec/op    │    sec/op     vs base               │
GrowLRUParallelAddGrow   756.90n ± 8%   96.58n ± 13%  -87.24% (p=0.002 n=6)
```

`CodeCache.Put` end-to-end does **not** move (500.2n → 497.1n, p=0.394):
`addrToHash` / `addrToCodeHash` are `hashicorp/golang-lru` caches behind
a single global mutex, and that dominates the put path. This PR removes
real CPU time from the content layers; unlocking it end-to-end needs
that addr LRU sharded, which is separate work.

Two tests pin counter == the LRU's real length: `growLRU` directly
across add / remove / eviction / grow / `Purge`, and all three
`CodeCache` layers under concurrent puts. Both verified to fail against
a mutated counter.

No TDD cycle: performance refactor with no intended behaviour change.

https://claude.ai/code/session_01WFkAYPPhqPe1NXg41Nph78
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.

2 participants