Go

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 range keeps its existing semantics.
  • Maps still never shrink: delete and clear(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 writes on 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

All 38 Performance, Profiling & Testing questions