Slice 擴(kuò)容后容量及內(nèi)存如何計(jì)算?
1. 擴(kuò)容后預(yù)估容量
假設(shè)現(xiàn)在有一個(gè)長(zhǎng)度為 2 的切片,對(duì)其進(jìn)行擴(kuò)容,增加三個(gè)元素
- sli := []int{1,2}
- sli = append(sli, 3, 4, 5)
對(duì)于擴(kuò)容后的切片,長(zhǎng)度為 5,這一點(diǎn)沒(méi)有任何爭(zhēng)議。
但容量呢?難道也是 5?
經(jīng)過(guò)運(yùn)行驗(yàn)證,實(shí)際的容量為 6 。
什么情況?這 6 是如何計(jì)算出來(lái)的呢?
這就不得不去 Go 源代碼中一探究竟,在 runtime/slice.go 關(guān)于 slice 擴(kuò)容增長(zhǎng)的代碼如下:
- newcap := old.cap
- if newcap+newcap < cap {
- newcap = cap
- } else {
- for {
- if old.len < 1024 {
- newcap += newcap
- } else {
- newcap += newcap / 4
- }
- if newcap >= cap {
- break
- }
- }
對(duì)于這段代碼,只要理解前兩行,其他的就不攻自破了
- 第一行的 old.cap:擴(kuò)容前的容量,對(duì)于此例,就是 2
- 第二行的 cap:擴(kuò)容前容量加上擴(kuò)容的元素?cái)?shù)量,對(duì)于此例,就是 2+3
整段代碼的核心就是要計(jì)算出擴(kuò)容后的預(yù)估容量,也就是 newcap,計(jì)算的具體邏輯是:
- 若 old.cap * 2 小于 cap,那 newcap 就取大的 cap
- 若 old.cap * 2 大于 cap,并且old.cap 小于 1024,那 newcap 還是取大,也即 newcap = old.cap * 2
- 若 old.cap * 2 大于 cap,但是old.cap 大于 1024,那兩倍冗余可能就有點(diǎn)大了,系數(shù)換成 1.25,也即 newcap = old.cap * 2
但 newcap 只是預(yù)估容量,并不是最終的容量,要計(jì)算最終的容量,還需要參考另一個(gè)維度,也就是內(nèi)存分配。
2. 內(nèi)存的分配規(guī)律
舉個(gè)現(xiàn)實(shí)中的例子來(lái)說(shuō)
你家里有五個(gè)人,每個(gè)人都想吃綠豆糕,因此你的需求就是 5,對(duì)應(yīng)上例中的 cap ,于是你就到超市里去買(mǎi)。
但超市并不是你家開(kāi)的,綠豆糕都是整盒整盒的賣(mài),沒(méi)有賣(mài)散的,每盒的數(shù)量是 6 個(gè),因此你最少買(mǎi) 6 個(gè)。
每次購(gòu)買(mǎi)的最少數(shù)量,就可以類(lèi)比做 Go 的內(nèi)存分配規(guī)律。
只有了解了 Go 的內(nèi)存分配規(guī)律,我們才能準(zhǔn)確的計(jì)算出我們最少得買(mǎi)多少的綠豆糕(得申請(qǐng)多少的內(nèi)存,分配多少的容量)。
關(guān)于內(nèi)存管理模塊的代碼,在 runtime/sizeclasses.go
- // class bytes/obj bytes/span objects tail waste max waste
- // 1 8 8192 1024 0 87.50%
- // 2 16 8192 512 0 43.75%
- // 3 32 8192 256 0 46.88%
- // 4 48 8192 170 32 31.52%
- ...
- // 17 256 8192 32 0 5.86%
- // 18 288 8192 28 128 12.16%
- // 19 320 8192 25 192 11.80%
- // 20 352 8192 23 96 9.88%
- // 21 384 8192 21 128 9.51%
- // 22 416 8192 19 288 10.71%
- // 23 448 8192 18 128 8.37%
- // 24 480 8192 17 32 6.82%
- // 25 512 8192 16 0 6.05%
- ...
- // 66 32768 32768 1 0 12.50%
從上面這個(gè)表格中,可以總結(jié)出一些規(guī)律。
- 在小于16字節(jié)時(shí),每次以8個(gè)字節(jié)增加
- 當(dāng)大于16小于2^8時(shí),每次以16字節(jié)增加
- 當(dāng)大于2^8小于2^9時(shí)以32字節(jié)增加
- 依此規(guī)律…
3. 匹配到合適的內(nèi)存
第一節(jié)中我們例子中,主人公是一個(gè)元素類(lèi)型為 int 的切片,每個(gè) int 占用為 8 個(gè)字節(jié),由于我們計(jì)算出的 newcap 為 5,因此新的切片,最少最少要占用 5*8 = 40 個(gè)字節(jié)。
再到第二節(jié)中的表格中查看,發(fā)現(xiàn)離 40 byte 最接近的是 32 和 48 兩個(gè)檔位。
如果是 32 byte,就是不夠用了,
因此 只能選擇 48 這個(gè)檔位去分配內(nèi)存。
有了實(shí)際分配的內(nèi)存,再反回去計(jì)算容量,就是擴(kuò)容后真實(shí)的切片容量,也就是 48/8 = 6
是不是很簡(jiǎn)單呢?