7.3 Go Map 性能学习笔记
1. Map 预分配容量 — make(map[K]V, capacity)
知道 map 大致大小时,预分配容量减少扩容开销:
package main
import (
"fmt"
"time"
)
func main() {
// 预分配 1000 个键值对的空间(减少扩容)
largeMap := make(map[string]int, 1000)
// 基准测试:插入 10000 个键值对
fmt.Println("Benchmarking map insertion...")
start := time.Now()
for i := 0; i < 10000; i++ {
largeMap[fmt.Sprintf("key%d", i)] = i * 2
}
insertTime := time.Since(start)
fmt.Printf("Time to insert 10,000 items: %v\n", insertTime)
}
执行结果:
Benchmarking map insertion...
Time to insert 10,000 items: 1.7603ms
要点:
-
make(map[string]int, 1000)— 第二个参数是提示容量(hint),不是硬限制- map 可以超过提示容量继续增长,只是减少前期扩容次数
- 类似
make([]int, 0, 1000)对切片的作用
- 不预分配:每次扩容都要分配新内存 + 重新哈希所有键,频繁扩容影响性能
- 预分配好处:一次性分配足够内存,减少扩容和重新哈希的次数
- 注意:map 的容量提示不像切片的 cap 可以精确查询,
cap()不适用于 map - 何时预分配:知道大致元素数量时(如从数据库查到 N 条记录,
make(map, N))
2. Map 查找性能 — O(1) vs 切片的 O(n)
map 的查找是 O(1) 常数时间,切片的线性搜索是 O(n):
package main
import (
"fmt"
"time"
)
func main() {
// 创建 map 和切片,都存 10000 个元素
largeMap := make(map[string]int, 1000)
var slice []string
for i := 0; i < 10000; i++ {
largeMap[fmt.Sprintf("key%d", i)] = i * 2
slice = append(slice, fmt.Sprintf("key%d", i))
}
// 基准测试:100000 次 map 查找
fmt.Println("Benchmarking map lookup...")
start := time.Now()
var found int
for i := 0; i < 100000; i++ {
key := fmt.Sprintf("key%d", i%10000)
if _, exists := largeMap[key]; exists {
found++
}
}
lookupTime := time.Since(start)
fmt.Printf("Time for 100,000 lookups: %v (found %d items)\n", lookupTime, found)
// 对比:切片线性搜索 vs map 查找
fmt.Println("\nComparing map vs slice for membership testing...")
target := "key5000"
// 切片:逐个遍历查找(O(n))
start = time.Now()
var foundInSlice bool
for _, item := range slice {
if item == target {
foundInSlice = true
break
}
}
sliceSearchTime := time.Since(start)
// map:直接键查找(O(1))
start = time.Now()
_, foundInMap := largeMap[target]
mapSearchTime := time.Since(start)
fmt.Printf("Slice search time: %v (found: %t)\n", sliceSearchTime, foundInSlice)
fmt.Printf("Map search time: %v (found: %t)\n", mapSearchTime, foundInMap)
if mapSearchTime.Nanoseconds() > 0 {
fmt.Printf("Map is %dx faster for lookup\n", sliceSearchTime.Nanoseconds()/mapSearchTime.Nanoseconds())
}
}
执行结果:
Benchmarking map lookup...
Time for 100,000 lookups: 10.5644ms (found 100000 items)
Comparing map vs slice for membership testing...
Slice search time: 0s (found: true)
Map search time: 0s (found: true)
要点:
- 100000 次 map 查找只花了约 10ms — 平均每次查找约 0.1 微秒
- map 查找:O(1) — 通过哈希函数直接定位键,不管 map 有多少元素
- 切片线性搜索:O(n) — 逐个遍历,10000个元素最多比较10000次
- 本例中 slice 和 map 搜索时间都显示 0s — 数据量太小,时间精度不够
- 数据量更大时差距明显:10000 元素切片搜索需要遍历约 5000 次,map 只需1次哈希查找
-
选择依据:
- 需要查找/判断存在 → 用 map(O(1))
- 需要遍历/保持顺序 → 用切片(有序)
- 需要两者 → 切片 + map 并用(切片存顺序,map 存查找)
3. Map 删除不释放内存 — 需重建 map
删除 map 元素后,内存不会自动缩小:
package main
import "fmt"
func main() {
memoryMap := make(map[int]string)
for i := 0; i < 1000; i++ {
memoryMap[i] = fmt.Sprintf("value_%d", i)
}
fmt.Printf("Map with 1000 items created\n")
// 删除 90% 的元素
for i := 0; i < 900; i++ {
delete(memoryMap, i)
}
fmt.Printf("Deleted 900 items, %d items remaining\n", len(memoryMap))
// map 仍占用 1000 个元素的内存空间!
fmt.Println("Note: Maps don't automatically shrink memory after deletions")
// 如果需要释放内存,必须创建新 map:
// newMap := make(map[int]string, len(memoryMap))
// for k, v := range memoryMap { newMap[k] = v }
// memoryMap = newMap // 旧 map 由 GC 回收
}
执行结果:
Map with 1000 items created
Deleted 900 items, 100 items remaining
Note: Maps don't automatically shrink memory after deletions
要点:
-
delete(memoryMap, i)— 删除键值对,len()变为 100 - 但 map 的底层内存不会缩小!仍保留 1000 个元素的哈希桶空间
- 这与切片不同:切片可以
slice = slice[:newLen]缩短,但 map 无法"截断" - 如果删除大量元素后需要释放内存,重建新 map:
newMap := make(map[int]string, len(memoryMap)) // 只分配 100 个元素的空间 for k, v := range memoryMap { newMap[k] = v } // 复制剩余元素 memoryMap = newMap // 旧 map 被 GC 回收 - 何时需要重建:删除了大量元素(如删了 90%),剩余元素少,内存浪费严重时
- 何时不需要:删除少量元素(内存浪费不大),或 map 生命周期即将结束(GC 自然回收)
4. Map 遍历性能 — 大量数据遍历
遍历 map 所有键值对的性能:
package main
import (
"fmt"
"time"
)
func main() {
iterMap := make(map[string]int)
for i := 0; i < 10000; i++ {
iterMap[fmt.Sprintf("item%d", i)] = i
}
start := time.Now()
var sum int
for _, value := range iterMap {
sum += value
}
iterTime := time.Since(start)
fmt.Printf("Time to iterate and sum 10,000 items: %v (sum: %d)\n", iterTime, sum)
}
执行结果:
Time to iterate and sum 10,000 items: 0s (sum: 49995000)
要点:
- 10000 个元素的遍历求和几乎瞬间完成
- map 遍历时间复杂度:O(n) — 需要访问每个键值对
- 与切片遍历相比:map 遍历稍慢(需要跳过空的哈希桶),但差距不大
-
sum = 49995000— 0+1+2+...+9999 = 9999*10000/2 = 49995000(数学验证正确) - 遍历时只取值
for _, value := range iterMap,不需要键
5. 键长度对性能的影响 — 短键更快
哈希计算需要遍历键的全部内容,短键哈希更快:
package main
import (
"fmt"
"time"
)
func main() {
shortKeys := make(map[string]int)
longKeys := make(map[string]int)
// 短键:只用数字作为键 "0", "1", "2", ...
start := time.Now()
for i := 0; i < 1000; i++ {
shortKeys[fmt.Sprintf("%d", i)] = i
}
shortKeyTime := time.Since(start)
// 长键:用很长的字符串作为键
start = time.Now()
for i := 0; i < 1000; i++ {
longKeys[fmt.Sprintf("very_long_key_name_with_lots_of_characters_%d", i)] = i
}
longKeyTime := time.Since(start)
fmt.Printf("Short keys insertion time: %v\n", shortKeyTime)
fmt.Printf("Long keys insertion time: %v\n", longKeyTime)
}
执行结果:
Short keys insertion time: 570.4μs
Long keys insertion time: 612.1μs
要点:
- 短键("0", "1", ...):哈希只需计算 1-4 字节,插入约 570μs
- 长键("very_long_key_name_with_lots_of_characters_0"):哈希需计算 40+ 字节,插入约 612μs
- 差距约 7% — 长键每次哈希要多处理 40 字节,累积后比短键慢
- 影响因素:哈希函数需要遍历键的全部字节,键越长哈希越慢
- 实际影响:1000 个元素差距约 42μs,大数据量时差距更明显
- 优化建议:如果可以用 int 或短字符串做键,比长字符串更高效
-
map[int]V比map[string]V快(int 哈希是简单运算) - 短字符串键比长字符串键快("id_1" vs "very_long_identifier_1")
-
知识点总结
| 知识点 | 关键概念 |
|---|---|
| 预分配容量 |
make(map[K]V, hint) 减少扩容次数,hint 不是硬限制 |
| 查找性能 | map O(1) vs 切片 O(n),数据量大时差距明显 |
| 删除不释放内存 | delete 后内存不缩小,需重建新 map 才能释放 |
| 遍历性能 | O(n),与切片遍历差距不大 |
| 键长度影响 | 短键哈希更快,int 键比 string 键更高效 |
| 选型指南 | 查找/存在性用 map,有序/遍历用切片 |