Slice 扩容后容量及内存如何计算?

开发 后端
只有了解了 Go 的内存分配规律,我们才能准确的计算出我们最少得买多少的绿豆糕(得申请多少的内存,分配多少的容量)。

[[429043]]

1. 扩容后预估容量

假设现在有一个长度为 2 的切片,对其进行扩容,增加三个元素

  1. sli := []int{1,2} 
  2. sli = append(sli, 3, 4, 5) 

对于扩容后的切片,长度为 5,这一点没有任何争议。

但容量呢?难道也是 5?

经过运行验证,实际的容量为 6 。

什么情况?这 6 是如何计算出来的呢?

这就不得不去 Go 源代码中一探究竟,在 runtime/slice.go 关于 slice 扩容增长的代码如下:

  1. newcap := old.cap 
  2. if newcap+newcap < cap { 
  3.     newcap = cap 
  4. else { 
  5.     for { 
  6.         if old.len < 1024 { 
  7.             newcap += newcap 
  8.         } else { 
  9.             newcap += newcap / 4 
  10.         } 
  11.         if newcap >= cap { 
  12.             break 
  13.         } 
  14.     } 

对于这段代码,只要理解前两行,其他的就不攻自破了

  • 第一行的 old.cap:扩容前的容量,对于此例,就是 2
  • 第二行的 cap:扩容前容量加上扩容的元素数量,对于此例,就是 2+3

整段代码的核心就是要计算出扩容后的预估容量,也就是 newcap,计算的具体逻辑是:

  1. 若 old.cap * 2 小于 cap,那 newcap 就取大的 cap
  2. 若 old.cap * 2 大于 cap,并且old.cap 小于 1024,那 newcap 还是取大,也即 newcap = old.cap * 2
  3. 若 old.cap * 2 大于 cap,但是old.cap 大于 1024,那两倍冗余可能就有点大了,系数换成 1.25,也即 newcap = old.cap * 2

但 newcap 只是预估容量,并不是最终的容量,要计算最终的容量,还需要参考另一个维度,也就是内存分配。

 2. 内存的分配规律

举个现实中的例子来说

你家里有五个人,每个人都想吃绿豆糕,因此你的需求就是 5,对应上例中的 cap ,于是你就到超市里去买。

但超市并不是你家开的,绿豆糕都是整盒整盒的卖,没有卖散的,每盒的数量是 6 个,因此你最少买 6 个。

每次购买的最少数量,就可以类比做 Go 的内存分配规律。

只有了解了 Go 的内存分配规律,我们才能准确的计算出我们最少得买多少的绿豆糕(得申请多少的内存,分配多少的容量)。

关于内存管理模块的代码,在 runtime/sizeclasses.go

  1. // class  bytes/obj  bytes/span  objects  tail waste  max waste 
  2. //     1          8        8192     1024           0     87.50% 
  3. //     2         16        8192      512           0     43.75% 
  4. //     3         32        8192      256           0     46.88% 
  5. //     4         48        8192      170          32     31.52% 
  6. ... 
  7. //    17        256        8192       32           0      5.86% 
  8. //    18        288        8192       28         128     12.16% 
  9. //    19        320        8192       25         192     11.80% 
  10. //    20        352        8192       23          96      9.88% 
  11. //    21        384        8192       21         128      9.51% 
  12. //    22        416        8192       19         288     10.71% 
  13. //    23        448        8192       18         128      8.37% 
  14. //    24        480        8192       17          32      6.82% 
  15. //    25        512        8192       16           0      6.05% 
  16. ... 
  17. //    66      32768       32768        1           0     12.50% 

从上面这个表格中,可以总结出一些规律。

  • 在小于16字节时,每次以8个字节增加
  • 当大于16小于2^8时,每次以16字节增加
  • 当大于2^8小于2^9时以32字节增加
  • 依此规律…

3. 匹配到合适的内存

第一节中我们例子中,主人公是一个元素类型为 int 的切片,每个 int 占用为 8 个字节,由于我们计算出的 newcap 为 5,因此新的切片,最少最少要占用 5*8 = 40 个字节。

再到第二节中的表格中查看,发现离 40 byte 最接近的是 32 和 48 两个档位。

如果是 32 byte,就是不够用了,

因此 只能选择 48 这个档位去分配内存。

有了实际分配的内存,再反回去计算容量,就是扩容后真实的切片容量,也就是 48/8 = 6

 

是不是很简单呢?

 

责任编辑:武晓燕 来源: Go编程时光
相关推荐

2023-11-28 11:44:54

Go切片

2010-06-02 11:33:26

Linux 内存监控

2021-01-18 05:15:30

存储容量服务器

2021-08-16 06:56:21

Slice数组类型内存

2023-03-13 13:36:00

Go扩容切片

2018-11-06 10:08:34

无线AP网络带宽无线网络

2021-08-12 14:19:14

Slice数组类型内存

2022-02-15 08:55:16

参数Go函数

2017-12-11 11:00:27

内存泄露判断

2021-01-04 10:54:58

云计算容量管理

2012-08-14 10:05:08

2017-08-28 15:32:52

内存DDR4DDR3

2009-02-16 18:08:01

linux硬件信息cpu

2014-03-14 13:32:52

cgroupsLinux容器

2010-08-04 10:10:03

光纤网络容量爆炸

2009-12-08 13:40:58

Windows XP系

2010-06-04 15:30:44

Linux 查看内存

2023-03-31 07:31:28

SliceGolang

2021-12-17 00:03:23

内存DDRGDDR

2016-05-25 10:03:51

JavaScript内存泄露
点赞
收藏

51CTO技术栈公众号