Go 1.24 replaced the built-in map implementation with Swiss tables. What changed, and what does it mean for performance-sensitive code?
Question 405HardGo 1.22 to 1.25
The old map was an array of buckets holding 8 entries each, with overflow buckets chained on collisions and incremental evacuation into a doubled array on growth. Go 1.24's implementation (internal/runtime/maps) is based on Swiss tables:
- Slots are organized in groups of 8, each with an 8-byte control word holding 7 bits of the hash (H2) per slot. A lookup uses the low hash bits (H1) to choose a starting group, then compares all 8 control bytes at once with bit tricks (SWAR, or SIMD on amd64), touching key memory only for probable matches. Probing moves group by group; there are no overflow chains.
- Higher maximum load factor (7/8), so maps use less memory for the same number of entries.
- A map is a directory of tables (extendible hashing). Each table holds at most 1024 slots and grows by splitting on its own, so no single insert has to rehash a huge map. That replaces the old incremental evacuation and keeps the worst-case insert latency bounded.
- Small maps (8 entries or fewer) are a single group with no directory or table overhead.
The Go team reported microbenchmark map operations up to ~60% faster, and about 1.5% geomean CPU improvement across full application benchmarks. What did not change:
- Iteration order is still randomized; deleting or adding entries during
rangekeeps its existing semantics. - Maps still never shrink:
deleteandclear(m)leave the tables allocated. To release memory, copy the live entries into a new map. - Presizing with
make(map[K]V, n)still avoids repeated splits. - Maps are still not safe for concurrent writes; the runtime still throws
fatal error: concurrent map writeson a detected race.
func BenchmarkMapInsert(b *testing.B) {
const n = 100_000
for _, presize := range []bool{false, true} {
b.Run(fmt.Sprintf("presize=%v", presize), func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var m map[int]int
if presize {
m = make(map[int]int, n)
} else {
m = make(map[int]int)
}
for i := range n {
m[i] = i
}
}
})
}
}
Go 1.24 and 1.25 let you opt out at build time with GOEXPERIMENT=noswissmap to check whether a regression is caused by the new maps. Interviewer is looking for: knowing that map performance depends on memory layout and cache misses, and that the observable semantics were deliberately kept the same.
More on Performance, Profiling & Testing
- Q403How do you write a test that fails if a hot path starts allocating? Explain testing.AllocsPerRun and its caveats.
- Q404How do you benchmark concurrent code with b.RunParallel, and why does sync.RWMutex often fail to scale for read-heavy workloads?
- Q406Why do pointers make the GC more expensive, and how can data layout, unique.Handle, and weak pointers reduce memory and GC cost?