Swiss Map in Go 1.24: Compatibility, Extendible Hashing, and Legacy Issues

Explore the groundbreaking Swiss Map in Go 1.24, which offers over 50% performance improvement and seamless migration from legacy maps. Discover the benefits of Extensible Hashing and its innovative design while addressing challenges like concurrency and memory efficiency. Is it time to adopt Swiss Map for your projects? Read more to find out!

 

In the previous article, I introduced swiss map and a Go implementation by Dolthub. Readers unfamiliar with swiss map should review that piece first.

With the upcoming release of Go 1.24, swiss map will replace the existing map implementation in the Go standard library. It maintains full API compatibility while delivering over 50% performance improvements in specific benchmark scenarios.
Currently, swiss map is my most anticipated feature in Go 1.24. But does it truly live up to the hype? This article analyzes its core design through three lenses: compatibility, Extensible Hashing implementation, and remaining challenges.

Compatibility: Seamless Migration Support

One of swiss map’s key design goals is backward compatibility with Go’s legacy map. Conditional compilation flags and type conversions enable zero-code migration. For example, in export_swiss_test.go, the newTestMapType function directly converts legacy map metadata into swiss map’s type structure:

1
2
3
4
5
6
7
// 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)) // Direct type conversion
    return mt
}

This design allows existing code to enable swiss map via the experimental flag GOEXPERIMENT=swissmap (now enabled by default in gotip builds like go1.24-3f4164f5).
To revert to the legacy map implementation, use GOEXPERIMENT=noswissmap.

Swiss Map’s Data Structure

Extendible Hashing: Efficient Incremental Scaling

Beyond compatibility improvements, swiss map introduces Extensible Hashing to enable efficient incremental scaling. Unlike traditional hash tables, which require complete data migration during resizing, Extensible Hashing distributes scaling costs across multiple operations using multi-level directories and table splitting.

Directory and Table Hierarchy

The Map struct in map.go uses globalDepth and directory to manage hierarchy:

1
2
3
4
5
6
// https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/map.go#L194
type Map struct {
    globalDepth  uint8       // Global depth of directory
    dirPtr       unsafe.Pointer // Pointer to directory (array of tables)
    // ...
}

The directory size is 1 << globalDepth, with each entry pointing to a table. When a table reaches its capacity (maxTableCapacity, default 1024), it triggers a split instead of global resizing.

Split Operation

A split creates two child tables (left and right) with increased localDepth, redistributing data based on hash bits:

1
2
3
4
5
6
7
8
// 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++ // Child tables have +1 local depth
    left := newTable(typ, maxTableCapacity, -1, localDepth)
    right := newTable(typ, maxTableCapacity, -1, localDepth)
    // ...
}

New tables allocate contiguous memory blocks via newarray, preserving cache locality.

Data Redistribution: Hash Masking

During splits, hash values’ high-order bits (determined by localDepth) dictate data placement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/table.go#L1052
mask := localDepthMask(localDepth) // e.g., 0x80000000 (32-bit)
for ... {
    hash := typ.Hasher(key, m.seed)
    if hash & mask == 0 {
        left.uncheckedPutSlot(...) // Assign to left table
    } else {
        right.uncheckedPutSlot(...) // Assign to right table
    }
}
  • Mask Calculation: localDepthMask generates masks like:
    • localDepth=10x80000000 (32-bit) or 0x8000000000000000 (64-bit)

Directory Expansion

When a split occurs at the global depth, the directory doubles in size:

1
2
3
4
5
6
7
8
9
// map.go
func (m *Map) installTableSplit(old, left, right *table) {
    if old.localDepth == m.globalDepth {
        newDir := make([]*table, m.dirLen*2) // Double directory size
        // Update directory entries...
        m.globalDepth++
    }
    // ...
}

Example: Directory Expansion
Initial state (globalDepth=1):

1
2
directory[0]  Table A (localDepth=1)
directory[1]  Table A (localDepth=1)

After split (globalDepth=2):

1
2
3
4
directory[0]  Left  (hash prefix 00)
directory[1]  Left  (hash prefix 01)
directory[2]  Right (hash prefix 10)
directory[3]  Right (hash prefix 11)

Key Advantages

  1. Locality: Only overloaded tables split
  2. Incremental Scaling: Directory grows on demand
  3. Cache Efficiency: Continuous memory allocation for table groups

Additional Optimizations

swiss map optimizes small-element scenarios (≤8 elements) using a single group, minimizing performance penalties for small datasets.

Remaining Challenges

Despite significant improvements, several issues remain:

Concurrency Limitations

The current implementation uses a simple writing flag for concurrent write detection:

1
2
3
4
5
// 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 // Non-atomic flag
    // ...
}

This may cause race conditions in high-concurrency scenarios. Future versions may introduce finer-grained locking.

Memory Fragmentation

The group structure (8 control bytes + 8 key-value slots) may waste memory for small types. For example, int32 keys with int8 values leave 3 bytes unused per slot.

Iterator Complexity

The Iter implementation handles directory expansion and table splits, adding complexity:

1
2
3
4
5
6
7
8
// https://github.com/golang/go/blob/3f4164f508b8148eb526fc096884dba2609f5835/src/internal/runtime/maps/table.go#L742
func (it *Iter) Next() {
    if it.globalDepth != it.m.globalDepth {
        // Handle directory expansion
        it.dirIdx <<= (it.m.globalDepth - it.globalDepth)
    }
    // ...
}

Frequent resizing may impact iterator performance.

Additionally, numerous TODOs remain in the latest codebase (see the image below), and questions about their resolution remain unresolved before Go 1.24’s release.

Community discussions highlight performance variability, with some reports of regressions.
https://x.com/valyala/status/1879988053076504761

Performance Testing

Test code: github.com/hxzhouh/gomapbench
Environment: go version devel go1.24-3f4164f5 Mon Jan 20 09:25:11 2025 -0800 darwin/arm64

Average performance improvements hover around 20%, with some scenarios showing up to 50% gains. However, these results are machine-specific, and some users report performance regressions. The final evaluation awaits further optimization.

Conclusion

Go 1.24’s swiss map delivers significant performance gains through compatibility design, Extendible Hashing, and optimized probing sequences. However, challenges remain in concurrency handling and memory efficiency. Developers should consider adopting it in performance-critical contexts while monitoring its evolving implementation.

References


  • Long Time Link
  • If you find my blog helpful, please subscribe to me via RSS
  • Or follow me on X
  • If you have a Medium account, follow me there. My articles will be published there as soon as possible.
Licensed under CC BY-NC-SA 4.0
Last updated on Jan 23, 2025 14:34 CST
Built with Hugo
Theme Stack designed by Jimmy