Repository navigation
runtime: map cold cache improvements #70835
Copy link
Copy link
Closed
Labels
NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.Issues related to the Go compiler and/or runtime.
Milestone
Description
There is room for improvement when some or all of a map is not in cache. The hierarchical design we use to enable incremental growth leaves us with several different allocations that can experience cache misses:
- The Map itself (on m.Used)
- The directory slice (on m.directoryAt)
- The table (on t.groups.lengthMask)
- The control word (on matchH2)
- The slot key (on key comparison)
- The slot value (on access in the caller)
(4), (5), and (6) are in the same object, so they may share a cache line depending on how far apart they are.
Some changes that may help (or may make things worse!):
- Grouping control words together (prototype).
- Good for locality of control words. Probably most useful for lookup of keys that aren't in the map (as the key has worse locality with the control word).
- Also good for optimistic prefetch of the first control word, something that Abseil does.
- Change the directory from
[]*tableto[]table(prototype).- Effectively merges cases (2) and (3).
- Store group data directly in the table. The table becomes variable size, with a
table8containing 1 group,table16containing 2 groups, and so on up totable1024.- Effectively merges cases (3) and (4).
- Allows optimistic prefetch of the control word / first key before even touching the table as they are at fixed offsets.
- We'd likely want to store the size in the directory as well to enable above.
- This is unfortunately mutually exclusive with the previous option, as the table is variable-sized.
There is also the question of whether to store the key and value together (KVKVKV...), as we do today, or to group the keys together (KKKVVV...), as we do in the old maps. Both cases have pros and cons:
K/V together:
- Map hit
- Cache miss on control word
- Cache miss on key comparison
- No cache miss on value use in parent
- Map miss
- Cache miss on control word
- Cache miss on key comparison in false positive matches (~1%)
- No value
All keys together:
- Map hit
- Cache miss on control word
- No cache miss on key comparison
- Cache miss on value use in parent (if used)
- Map miss
- Cache miss on control word
- No cache miss on key comparison in false positive matches (~1%)
- No value
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.Issues related to the Go compiler and/or runtime.
Type
Projects
- StatusShow more project fieldsDone
Activity
gabyhelp commentedon Dec 13, 2024
Related Issues
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
bboreham commentedon Apr 16, 2025
I noticed Prometheus uses more CPU using Go 1.24, in a couple of places where it is accessing a large map.
This was hard to see with existing benchmarks, so I modified this benchmark to generate more data (and stripped out extra test cases to make it easier to run).
Check out that branch and run like this:
Using Go 1.23 I get 82 seconds of CPU in
runtime.mapaccess1_fast64; with Go 1.24.2 it's 108 seconds.Profile from the latter:
cpu.pprof.gz
[Gnarly extra info: Prometheus shards this map 16,000 ways by default, to reduce lock contention. But when I change that number down to 128 this benchmark still looks much the same.]
gopherbot commentedon Oct 14, 2025
Change https://go.dev/cl/711560 mentions this issue:
internal/runtime/maps: use KKKKVVVV slot ordergopherbot commentedon Mar 24, 2026
Change https://go.dev/cl/758760 mentions this issue:
main.star: add mapsplitgroup builderinternal/runtime/maps: add GOEXPERIMENT=mapsplitgroup for KKKKVVVV sl…
main.star: add mapsplitgroup builder
prattmic commentedon Mar 24, 2026
https://go.dev/cl/711560 adds GOEXPERIMENT=mapsplitgroup, switching to the "All keys together" variant described above. My intention is to evaluate performance on several production workloads to determine if the change is neutral or better (neutral is OK because #71368 is improved). Hopefully we can decide whether to commit to this before the 1.27 freeze.
prattmic commentedon Aug 24, 2026
I've (finally) completed an analysis of
GOEXPERIMENT=mapsplitgroupon Google production workloads, and found it was neutral on cache effects: no change to IPC or LLC cache misses. Given the reduced padding in the memory layout is an improvement, I think this is worth enabling by default.gopherbot commentedon Aug 24, 2026
Change https://go.dev/cl/820500 mentions this issue:
internal/buildcfg: enable GOEXPERIMENT=mapsplitgroup by defaultinternal/buildcfg: enable GOEXPERIMENT=mapsplitgroup by default