Go源碼分析之預(yù)分配slice內(nèi)存
切片擴容
在 1.18 版本前,切片擴容,在容量小于1024時,以2倍大小擴容。超過1024后,以1.25倍擴容。在擴容后切片的基礎(chǔ)上,會根據(jù)長度和容量進(jìn)行 roundupsize 。
在1.18版本后,接下來看一下源碼如下:
func growslice(et *_type, old slice, cap int) slice { // ...... newcap := old.cap doublecap := newcap + newcap //雙倍擴容(原容量的兩倍) if cap > doublecap { //如果所需容量大于 兩倍擴容,則直接擴容到所需容量 newcap = cap } else { const threshold = 256 //這里設(shè)置了一個 閾值 -- 256 if old.cap < threshold { //如果舊容量 小于 256,則兩倍擴容 newcap = doublecap } else { // 檢查 0 < newcap 以檢測溢出并防止無限循環(huán)。 for 0 < newcap && newcap < cap { //如果新容量 > 0 并且 原容量 小于 所需容量 // 從小片的增長2x過渡到大片的增長1.25x。這個公式給出了兩者之間的平滑過渡。 newcap += (newcap + 3*threshold) / 4 //新容量是 = 1.25 原容量 + 3/4 閾值 (192) } //當(dāng)newcap計算溢出時,將newcap設(shè)置為請求的上限。 if newcap <= 0 { // 如果發(fā)生了溢出,將新容量設(shè)置為請求的容量大小 newcap = cap } } } }
函數(shù)判斷如果所需容量 cap
大于兩倍擴容的容量 doublecap
,說明 cap
的需求已經(jīng)超過了兩倍擴容的范圍,所以將 newcap
直接設(shè)為 cap
。
否則,如果原容量 old.cap
小于一個閾值 threshold
(這里設(shè)為256),則將 newcap
設(shè)置為原容量的兩倍 doublecap
。
如果既不滿足上述條件,則進(jìn)入一個循環(huán),只要 newcap
大于0且小于所需容量 cap
,就會進(jìn)入循環(huán)。在每次循環(huán)迭代中,newcap
的增長方式為當(dāng)前 newcap
加上 3*threshold
的四分之一。這個計算方式使得容量的增長逐漸從原容量的兩倍過渡到1.25倍,實現(xiàn)了一個平滑的過渡。
最后,在循環(huán)結(jié)束后,判斷如果 newcap
仍然小于等于0(溢出情況),則將 newcap
設(shè)為所需容量 cap
如果 現(xiàn)有容量 小于 256 ,則新容量是原來的兩倍
新容量 = 1.25 原容量 + 3/4 閾值 (192) “這個公式給出了從1.25倍增長 過渡到2 倍增長,兩者之間的平滑過渡。” 在此情況下,如果發(fā)生了溢出,將新容量設(shè)置為請求的容量大小
代碼測試
func main() { slice := make([]int, 0) for i := 0; i < 512; i++ { slice = append(slice, i) } newSlice := append(slice, 5000) fmt.Printf("Before Pointer = %p, len = %d, cap = %d\n", &slice, len(slice), cap(slice)) fmt.Printf("Before Pointer = %p, len = %d, cap = %d\n", &newSlice, len(newSlice), cap(newSlice)) }
執(zhí)行輸出如下:
再看一個例子
下面用 go1.17 和 go1.18 兩個版本來分開說明。先通過一段測試代碼,直觀感受一下兩個版本在擴容上的區(qū)別。
package?main import?"fmt" func?main()?{ ????s?:=?make([]int,?0) ????oldCap?:=?cap(s) ????for?i?:=?0;?i?<?2048;?i++?{ ????????s?=?append(s,?i) ????????newCap?:=?cap(s) ????????if?newCap?!=?oldCap?{ ????????????fmt.Printf("[%d?->?%4d]?cap?=?%-4d??|??after?append?%-4d??cap?=?%-4d\n",?0,?i-1,?oldCap,?i,?newCap) ????????????oldCap?=?newCap ????????} ????} }
運行結(jié)果(1.17 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 1024
[0 -> 1023] cap = 1024 | after append 1024 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1696
[0 -> 1695] cap = 1696 | after append 1696 cap = 2304
在分配內(nèi)存空間之前需要先確定新的切片容量,運行時根據(jù)切片的當(dāng)前容量選擇不同的策略進(jìn)行擴容:
- 如果期望容量大于當(dāng)前容量的兩倍就會使用期望容量;
- 如果當(dāng)前切片的長度小于 1024 就會將容量翻倍;
- 如果當(dāng)前切片的長度大于等于 1024 就會每次增加 25% 的容量,直到新容量大于期望容量;
運行結(jié)果(1.18 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 848
[0 -> 847] cap = 848 | after append 848 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1792
[0 -> 1791] cap = 1792 | after append 1792 cap = 2560
- 如果期望容量大于當(dāng)前容量的兩倍就會使用期望容量;
- 如果當(dāng)前切片的長度小于閾值(默認(rèn) 256)就會將容量翻倍;
- 如果當(dāng)前切片的長度大于等于閾值(默認(rèn) 256),就會每次增加 25% 的容量,基準(zhǔn)是
newcap + 3*threshold
,直到新容量大于期望容量;
內(nèi)存對齊
但是,后半部分還對 newcap
作了一個內(nèi)存對齊
,這個和內(nèi)存分配策略相關(guān)。進(jìn)行內(nèi)存對齊之后,新 slice 的容量是要 大于等于
按照前半部分生成的newcap
。
之后,向 Go 內(nèi)存管理器申請內(nèi)存,將老 slice 中的數(shù)據(jù)復(fù)制過去,并且將 append 的元素添加到新的底層數(shù)組中。
最后,向 growslice
函數(shù)調(diào)用者返回一個新的 slice,這個 slice 的長度并沒有變化,而容量卻增大了。
測試代碼
import "fmt" func main() { s := []int{1,2} s = append(s,4,5,6) fmt.Printf("len=%d, cap=%d",len(s),cap(s)) }
輸出
len=5, cap=6
根據(jù)Go語言中切片的擴容機制,當(dāng)切片容量不足以容納額外的元素時,它會自動進(jìn)行擴容。在這種情況下,切片的容量會根據(jù)需要自動增長,通常會以原來容量的2倍進(jìn)行擴容。
根據(jù)您的代碼,初始切片s的容量為2,添加了3個元素后,長度變?yōu)?。實際上,在這種情況下,切片的容量不會立即擴大到8,而是繼續(xù)保持為6。這是因為Go語言的切片擴容機制會優(yōu)化以減少內(nèi)存的浪費。擴容時,切片會選擇一個合適的容量,使得容量盡可能靠近但大于所需的最小容量。
源碼分析
func growslice(et *_type, old slice, cap int) slice { // …… newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { // …… } // …… capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize) }
capmem = roundupsize(uintptr(newcap) * ptrSize)
:根據(jù) newcap
和指針的大小計算需要分配的內(nèi)存大小。 newcap = int(capmem / ptrSize)
:通過將內(nèi)存大小除以指針大小,將新的容量 newcap
轉(zhuǎn)換為整數(shù)。
到此這篇關(guān)于Go源碼分析之預(yù)分配slice內(nèi)存的文章就介紹到這了,更多相關(guān)Go預(yù)分配slice內(nèi)存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言如何利用Mutex保障數(shù)據(jù)讀寫正確
這篇文章主要介紹了互斥鎖的實現(xiàn)機制,以及?Go?標(biāo)準(zhǔn)庫的互斥鎖?Mutex?的基本使用方法,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考一下2023-05-05Go并發(fā):使用sync.WaitGroup實現(xiàn)協(xié)程同步方式
這篇文章主要介紹了Go并發(fā):使用sync.WaitGroup實現(xiàn)協(xié)程同步方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05Go返回int64類型字段超出javascript Number范圍的解決方法
這篇文章主要介紹了Go返回int64類型字段超出javascript Number范圍的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07GoRoutines高性能同時進(jìn)行多個Api調(diào)用實現(xiàn)
這篇文章主要為大家介紹了GoRoutines高性能同時進(jìn)行多個Api調(diào)用實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03使用go net實現(xiàn)簡單的redis通信協(xié)議
本文主要介紹了go net實現(xiàn)簡單的redis通信協(xié)議,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12