golang中slice擴容的具體實現
Go 語言中的切片擴容機制是 Go 運行時的一個關鍵部分,它確保切片在動態(tài)增加元素時能夠高效地管理內存。這個機制是在 Go 運行時內部實現的,涉及了內存分配、數據拷貝和容量調整。擴容的實現主要體現在 runtime.growslice
函數中。下面我們將深入分析 Go 切片的擴容機制,結合源碼來進行詳細解答。
1. 切片擴容的觸發(fā)
切片是 Go 中的一種動態(tài)數組,它有 長度(len) 和 容量(cap)。當我們向切片添加元素時,切片的長度會增加。如果長度超出了當前的容量,Go 會自動擴容。切片擴容的過程是通過 append
函數來觸發(fā)的。
append 函數的實現
append
函數是 Go 內置的函數,用于向切片追加元素。它的實現會首先檢查當前切片的容量是否足夠容納新的元素,如果不夠,它會調用 runtime.growslice
函數來擴容。
// append() 源碼簡化版 func append(slice []T, elems ...T) []T { // 1. 檢查當前切片是否有足夠的容量 if len(slice)+len(elems) <= cap(slice) { // 如果容量足夠,直接追加元素 slice = slice[:len(slice)+len(elems)] copy(slice[len(slice)-len(elems):], elems) return slice } // 2. 如果容量不足,進行擴容 return growslice(reflect.TypeOf(slice).Elem(), slice, len(slice)+len(elems)) }
2. runtime.growslice 函數
當切片容量不足時,append
會調用 growslice
函數來擴容。growslice
函數是 Go 運行時內部函數,負責實際的內存分配和切片擴容操作。
growslice 函數源碼
// growslice 擴容的實現 func growslice(et *_type, old slice, cap int) slice { if cap < old.cap { panic("growslice: cap out of range") } // 如果元素大小為 0,直接返回新的切片 if et.size == 0 { return slice{unsafe.Pointer(&zerobase), old.len, cap} } // 擴容策略:根據當前容量和預期的新容量來決定新容量 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += (newcap + 3*threshold) / 4 } if newcap <= 0 { newcap = cap } } } // 根據擴容后的容量計算所需的內存空間 var lenmem, newlenmem, capmem uintptr switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) newcap = int(capmem) case et.size == goarch.PtrSize: lenmem = uintptr(old.len) * goarch.PtrSize newlenmem = uintptr(cap) * goarch.PtrSize capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) newcap = int(capmem / goarch.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if goarch.PtrSize == 8 { shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, _ = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } // 分配新的內存,并拷貝舊的數據 var p unsafe.Pointer if et.ptrdata == 0 { p = mallocgc(capmem, nil, false) } else { p = mallocgc(capmem, et, true) } memmove(p, old.array, lenmem) return slice{p, old.len, newcap} }
3. 擴容的具體過程
檢查容量是否足夠:
growslice
首先會檢查請求的擴容是否超過了當前切片的容量。如果新容量大于原容量的兩倍,它會直接使用新容量作為目標容量。根據擴容策略計算新容量:
如果切片的容量小于 256,它會直接翻倍容量(
newcap = cap
)。如果切片容量較大,擴容時會采取逐步增加的策略:按原容量的 1.25 倍+192 (也就是3*threshold / 4) 增長,直到滿足目標容量。
計算內存所需大?。?nbsp;
growslice
會計算新切片所需的內存大小,并分配新的內存空間。對于不同類型的元素(如基本類型、指針類型等),內存分配的方式會有所不同。內存分配和數據拷貝:
通過
mallocgc
分配新的內存區(qū)域,在分配空間的時候因根據標簽的大小等級會向上取整,go在擴容的時候寧愿犧牲一部分內部碎片也要保證外部盡可能少的外部碎片,并使用memmove
將原切片的數據拷貝到新的內存中。擴容后的切片會更新 指針(指向新的內存)、長度 和 容量,然后返回新的切片。
4. 擴容的性能考慮
切片擴容的過程需要進行 內存分配 和 數據拷貝,這會影響性能,尤其是在頻繁擴容的場景中。
頻繁擴容的開銷:每次擴容時,都會重新分配內存并將數據拷貝到新的內存區(qū)域,這會帶來額外的時間和空間開銷。
容量預估:為了避免頻繁擴容,最好在創(chuàng)建切片時預估切片需要的容量,尤其是在元素數量已知的情況下??梢允褂?nbsp;
make([]T, 0, n)
來預分配一個足夠的容量,以減少擴容操作。
5. 擴容策略的總結
小容量時(比如容量小于 256),切片會按 2 倍擴容。
大容量時,Go 會使用 1.25 倍擴容 + 192 的策略,確保不會過度浪費內存。
對于
nil
或零容量切片,growslice
會初始化并分配合適的容量。
6. 示例:切片擴容過程
下面是一個示例,展示了切片擴容的過程:
package main import "fmt" func main() { var slice []int for i := 0; i < 10; i++ { slice = append(slice, i) fmt.Printf("Length: %d, Capacity: %d, Slice: %v\n", len(slice), cap(slice), slice) } }
輸出示例:
Length: 1, Capacity: 1, Slice: [0]
Length: 2, Capacity: 2, Slice: [0 1]
Length: 3, Capacity: 4, Slice: [0 1 2]
Length: 4, Capacity: 4, Slice: [0 1 2 3]
Length: 5, Capacity: 8, Slice: [0 1 2 3 4]
Length: 6, Capacity: 8, Slice: [0 1 2 3 4 5]
Length: 7, Capacity: 8, Slice: [0 1 2 3 4 5 6]
Length: 8, Capacity: 8, Slice: [0 1 2 3 4 5 6 7]
Length: 9, Capacity: 16, Slice: [0 1 2 3 4 5 6 7 8]
Length: 10, Capacity: 16, Slice: [0 1 2 3 4 5 6 7 8 9]
如上所示,切片的容量在每次添加元素時會根據擴容策略進行增長。初始容量為 1,當容量不足時,擴容后容量翻倍。最終,容量達到 16。
總結
Go 的切片擴容機制是通過 append
函數和 growslice
函數實現的。在擴容時,Go 會根據當前切片的容量和預期的容量進行計算,并分配新的內存空間,拷貝原有數據。擴容的策略通常是小容量時翻倍,大容量時逐步增加,保證了內存的高效使用。
到此這篇關于golang中slice擴容的具體實現的文章就介紹到這了,更多相關golang slice擴容內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
gorm golang 并發(fā)連接數據庫報錯的解決方法
今天小編就為大家分享一篇gorm golang 并發(fā)連接數據庫報錯的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07go內存隊列l(wèi)ist VS slice實現方式對比分析
這篇文章主要為大家介紹了go內存隊列l(wèi)ist VS slice實現方式對比分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08