7.3 Go Map 性能学习笔记

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]Vmap[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,有序/遍历用切片
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容