golang / golang/go

internal/runtime/maps: tune wyhash/AES threshold against latency, not throughput

Open
#80,970 4 comments 0 reactions 0 assignees View on GitHub
compiler/runtime NeedsInvestigation Performance WaitingForInfo
Dominant language
Go
Stars
139k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

In https://go-review.googlesource.com/c/go/+/815960, I improved x86_64 benchmarks for small key maps by a pretty significant margin by switching AES *off* for very small keys.

The bakeoff benchmark I added actually measures two variants of this: a "latency" benchmark, where each call into memhash has a data dependency on the previous, and a "throughput" benchmark, where they do not. Currently, the tuning parameter I added selects for throughput, simply because I did not want to regress benchmarks. However, I believe these benchmarks are not realistic, and I would like to discuss more realistic benchmarks so we can tune this a little better.

So whats the deal with this discrepancy? Let's talk about hash collisions. SwissTable is cleverly designed so that you almost always find what you're looking for in the first group, so we can assume we never need to look at other groups and the BTB learns that. However, whether we need to make a key comparison is largely data-dependent, not code-path dependent, so it cannot be predicted. For example, filling a map with unique keys will never make key comparisons so this branch will be learned, but filling a map with a variety of overlapping keys will slam the breaks on the pipeline half the time because of mis-speculation.

Let's talk about AESNI. AESNI is a collection of vector instructions, which means that getting their results into a GPR requires porting the result from one register file to another, which is slow. The cost is constant per hash, so the relative cost drops off with key length. However, some workloads do much better, because they happen to predict the above key comparison very well, meaning that speculation can reorder things such that nothing stalls on this vector->gpr copy. This is why the runtime benchmarks seem to get worse when we switch to latency tuning: they happen to work!

So what are some realistic map use cases we should study? Here's some ideas.

1. RMW aggregation, e.g. `m[k].acc += v`. The address written to depends on the hash; latency-bound, especially in a loop, since all future RMWs might depend on the one we just did.
2. Deduplication: `if _, ok := seen[k]; !ok { seen[k] = v }`. Essentially equivalent to `m[k] = v`, so mostly throughput-bound.
3. Traversal through a map-based graph: `v = v.children[e]`. Which map we look at next depends on the hash, so this is extremely latency-bound.
4. Membership filter: `for _, x := range xs { v, ok := m[k]; if !ok { continue } ... }`. Can go either way, depending on whether the branch is 50-50 or 10-90, since in the latter case it can be predicted and we go into throughput land.

I would argue that most cases where maps are used are going to be leaning in the latency direction, so I think that biasing the tuning parameter towards the throughput end seems wise.

Now, how on earth do we macro-benchmark this effectively? I'm interested in ideas. The classic of course would be turning it on, waiting for Google to pick it up, and see what GWP says.

Contributor guide

Open the contributing guide

Research direction

Read the Go change under review 815960 and the bakeoff benchmarks it added for small-key map hashing. Compare the proposed RMW, deduplication, graph traversal, and membership-filter workloads, then determine what macro-benchmark evidence would justify changing the AES/wyhash threshold; done means an agreed realistic benchmark approach and tuning direction.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
performance
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.