Can Go 1.24’s Swiss Map Really Boost Performance? A Deep Dive into Its Design
This article examines Go 1.24’s new Swiss map implementation, covering its compatibility with the legacy map, the extendible hashing mechanism that enables incremental expansion, performance improvements, and remaining challenges such as concurrency limits and memory fragmentation.
Compatibility: Seamless Migration Support
Go’s Swiss map is designed to be compatible with the existing map type. Conditional compilation tags and type conversion allow a painless switch between the old and new implementations. For example, in export_swiss_test.go the newTestMapType function directly casts the metadata of a traditional map to the Swiss map type.
//https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/export_swiss_test.go#L14
func newTestMapType[K comparable, V any]() *abi.SwissMapType {
var m map[K]V
mTyp := abi.TypeOf(m)
mt := (*abi.SwissMapType)(unsafe.Pointer(mTyp)) // 直接类型转换
return mt
}This design enables existing code to enable the Swiss map via the experimental flag GOEXPERIMENT=swissmap without code changes. To keep using the old map, set GOEXPERIMENT=noswissmap.
Swiss Map Data Structure
Extendible Hashing: How Dynamic Expansion Works
The core innovation of the Swiss map is the use of Extendible Hashing to support efficient incremental expansion. Traditional hash tables require full rehashing on resize, whereas Extendible Hashing splits tables incrementally using a multi‑level directory.
Dir and Table Hierarchy
In map.go the Map struct contains globalDepth and directory. The directory size is 1 << globalDepth, each entry points to a table. When a table’s load exceeds maxTableCapacity (default 1024), a split is triggered.
//https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/map.go#L194
type Map struct {
globalDepth uint8 // dir的全局深度
dirPtr unsafe.Pointer // dir指针(指向多个 table)
// ...
}Split Operation
During a split, the original table (e.g., table A) creates two child tables ( Left and Right) whose localDepth is one greater than the original. The split redistributes entries based on the high‑order bits of the hash determined by localDepthMask.
//https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/table.go#L1043
func (t *table) split(typ *abi.SwissMapType, m *Map) {
localDepth := t.localDepth
localDepth++ // 子表的 localDepth 比原表大 1
left := newTable(typ, maxTableCapacity, -1, localDepth)
right := newTable(typ, maxTableCapacity, -1, localDepth)
// ...
}Directory Update and Expansion
After splitting, the global directory may need to double in size if the original table’s localDepth equals the current globalDepth. The directory entries are then updated to point to the new tables.
// map.go
func (m *Map) installTableSplit(old, left, right *table) {
if old.localDepth == m.globalDepth {
// 目录扩展:大小翻倍
newDir := make([]*table, m.dirLen*2)
for i := range m.dirLen {
newDir[2*i] = left
newDir[2*i+1] = right
}
m.dirPtr = unsafe.Pointer(&newDir[0])
m.globalDepth++
} else {
// 不扩展目录,仅替换部分项
entries := 1 << (m.globalDepth - left.localDepth)
for i := 0; i < entries; i++ {
m.directorySet(uintptr(old.index+i), left)
m.directorySet(uintptr(old.index+i+entries), right)
}
}
}Key Design Advantages
Locality : Only heavily loaded tables are split, leaving others untouched.
Incremental Expansion : The directory grows on demand, avoiding a massive one‑time migration.
Address Continuity : New tables allocate a contiguous memory block for groups.data, improving cache performance.
Other Optimizations
The Swiss map uses a single group to store up to eight elements, reducing overhead for small maps.
Legacy Issues and Challenges
Concurrency Limitations
The current implementation uses a simple writing flag to detect concurrent writes, lacking fine‑grained locking. This can cause race conditions under high contention.
//https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/map.go#L478
func (m *Map) PutSlot(typ *abi.SwissMapType, key unsafe.Pointer) unsafe.Pointer {
m.writing ^= 1 // 简单标志位,非原子操作
// ...
}Memory Fragmentation
The group structure (8 control bytes + 8 key/value slots) can waste space when keys and values are small, e.g., int32 keys with int8 values.
Iterator Complexity
The iterator must handle directory expansion and table splits, which adds overhead in frequently resizing scenarios.
// https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/table.go#L742
func (it *Iter) Next() {
if it.globalDepth != it.m.globalDepth {
// 处理目录扩展后的索引调整
it.dirIdx <<= (it.m.globalDepth - it.globalDepth)
}
// ...
}Performance Testing
Benchmarks from the author’s gomapbench repository show an average 28 % speedup, with peaks up to 50 % in some workloads. However, the tests are limited to a single laptop and some reports indicate performance regressions in certain cases.
Conclusion
Go 1.24’s Swiss map brings significant performance gains in high‑load scenarios through compatibility design, Extendible Hashing, and targeted optimizations. Nevertheless, its concurrency model and memory efficiency still have room for improvement, so developers should evaluate it carefully for performance‑critical code.
References
https://tonybai.com/2024/11/14/go-map-use-swiss-table/
https://github.com/golang/go/issues/54766
https://pub.huizhou92.com/swisstable-a-high-performance-hash-table-implementation-3e13bfe8c79b
https://www.geeksforgeeks.org/extendible-hashing-dynamic-approach-to-dbms/
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
