为什么 Go 对长度为 100k 的切片使用的内存比长度为 100k 的数组少?

Why does Go use less memory for a slice of length 100k, than for an array of length 100k?

提问人:Dan 提问时间:8/15/2023 最后编辑:Dan 更新时间:8/15/2023 访问量:113

问:

请考虑以下代码,其中我分配了 4000 个数组,每个数组的长度为 100k:

    parentMap := make(map[int][100_000]int)
    for i := 0; i < 4000; i++ {
        parentMap[i] = [100_000]int{}
        time.Sleep(3 * time.Millisecond)
    }

如果我在本地运行它并分析其内存使用情况,它会开始使用 >2GB 的内存。

现在,如果我们稍微更改代码以使用数组切片(但长度为 100k),如下所示:

    parentMap := make(map[int][]int)
    for i := 0; i < 4000; i++ {
        parentMap[i] = make([]int, 100_000)
        time.Sleep(3 * time.Millisecond)
    }

在我的机器上,内存峰值约为 73MB。为什么会这样?

我认为这两个片段将使用大致相等的内存,推理如下:

  • 在这两种情况下,Go 运行时都会在堆上分配 的值。Go 之所以这样做,是因为如果它在堆栈上分配了这些值,那么一旦当前函数超出范围,这些值就会全部清除。parentMapparentMap
  • 因此,第一个代码段直接在堆上分配 4k 个数组。
  • 并且,第二个代码段在堆上分配 4k 个切片标头。每个切片标头都有一个指向大小为 100k 的唯一数组(也在堆上)的指针。
  • 在这两种情况下,大小为 100k 的堆上都有 4k 数组。因此,无论哪种情况,都应使用大致相等的内存量。

我读到:https://go.dev/blog/slices-intro。但是找不到解释这一点的实现细节。

数组 堆内存 堆栈内存

评论

5赞 JimB 8/15/2023
数组是值,您正在将每个值复制到映射中。切片仅包含指向基础数组的指针。
0赞 Dan 8/15/2023
正确。但是,分片的基础数组(在堆上分配)必须占用与直接用作值的数组一样多的内存;这就是我感到困惑的原因
0赞 JimB 8/15/2023
这两个示例都显示了我的系统上大致相同的总分配。您可能正在查看操作系统报告的使用情况,其中没有考虑未使用的页面。
0赞 Dan 8/15/2023
你是怎么检查的?上面的结果是我从MacOS上的活动监视器中获得的。但是我刚刚检查了 pprof,得到了类似的结果(1.05GB 第一个片段,512kb 秒)
1赞 JimB 8/15/2023
运行时内存统计信息将准确显示运行时分配的内容。阵列版本还显示操作系统中切片的199876页面回收率与 3829 页回收率,操作系统只是没有报告您未使用的内存。将 0 写入切片,看看会发生什么。

答:

7赞 user2357112 8/15/2023 #1

带有切片的版本可能受益于延迟分配。没有任何东西会尝试写入其中一个切片的数据缓冲区,因此操作系统可以自由地不为这些缓冲区分配内存,直到有东西真正尝试写入。(OS 也可以延迟地将缓冲区初始化为零,因此不会强制分配。

同时,包含数组的版本需要实际将数组复制到映射中,这意味着实际执行写入。即使写入的值都是零,它们仍然是写入的,因此操作系统必须实际为要写入的数据分配内存。

尝试将数据写入这些切片,切片版本也应该占用千兆字节的内存。(我认为每页内存一个值就足够了,但用 s 填充切片可能更容易。1

评论

0赞 Dan 8/15/2023
我认为你是对的,我用切片在版本中添加了一个循环,用 0 填充它,内存也增加到了 GB,根据 Activity Monitor 的说法。谢谢!