sync.Map 什么时候真的比 map 加锁快?这篇用同一个一万键的负载,在 1 个和 8 个 goroutine、从只读到 50% 写的组合下量 sync.Map、RWMutex+map、Mutex+map,再看内存开销,并读源码确认它的实现。
单位 ns/次;顺序:sync.Map / RWMutex+map / Mutex+map 只读(0% 写,共享键) G=1: 29.6 / 16.8 / 17.0 只读(0% 写,共享键) G=8: 5.2 / 131.9 / 154.6 99% 读 1% 写(共享键) G=1: 33.8 / 17.3 / 17.3 99% 读 1% 写(共享键) G=8: 6.1 / 98.3 / 157.4 50% 读 50% 写(共享键) G=1: 68.8 / 23.5 / 21.7 50% 读 50% 写(共享键) G=8: 32.0 / 85.9 / 195.6 50% 读 50% 写(每个 goroutine 只碰自己的键区间) G=1: 74.1 / 29.6 / 23.3 50% 读 50% 写(每个 goroutine 只碰自己的键区间) G=8: 31.8 / 94.7 / 193.9
8 个 goroutine 读多写少,sync.Map 每次 5.2–6.1 ns,RWMutex+map 98–132 ns,Mutex+map 约 155 ns;写占到 50% 时 sync.Map 仍是 32 ns,明显优于另外两者(86–95 ns 和约 195 ns)。但只有 1 个 goroutine 时,sync.Map 29.6 ns,加锁的普通 map 只要 16.8–17.0 ns;50% 写时 68.8 对约 22 ns。没有并发竞争的时候不要用它。文档说“键互不相交”是它优化的第二种场景,我专门测了:50% 写、G=8 时 31.8 ns,和共享键的 32.0 ns 几乎一样,在这个测法下没有看到额外优势。
100 万个 int→int:普通 map 占堆 37.8 MB;sync.Map 占堆 121.6 MB(3.2 倍)
100 万个 int→int,sync.Map 占堆 121.6 MB,普通 map 37.8 MB。键和值都要装箱成 any,再加上字典树的节点(这个原因是我的推断,没有逐项量)。
Map is like a Go map[any]any but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes run in amortized constant time. The Map type is specialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content. The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may src/sync/map.go 里 Map 结构体的核心字段:m isync.HashTrieMap[any, any] internal/sync/hashtriemap.go:HashTrieMap is an implementation of a concurrent hash-trie. The implementation is designed around frequent loads, but offers decent performance for stores and deletes as well, especially if the map is larger. Its primary use-case is the unique package, but can be used elsewhere as well.
1.27.1 里 sync.Map 的核心字段是 isync.HashTrieMap[any, any],一个并发哈希字典树,注释说它围绕频繁的 Load 设计。我没有读它的具体算法,上面的快慢是实测。文档的态度仍然是:大多数代码应该用普通 map 加单独的锁。