golang中slice擴(kuò)容的具體實(shí)現(xiàn)
Go 語言中的切片擴(kuò)容機(jī)制是 Go 運(yùn)行時(shí)的一個(gè)關(guān)鍵部分,它確保切片在動(dòng)態(tài)增加元素時(shí)能夠高效地管理內(nèi)存。這個(gè)機(jī)制是在 Go 運(yùn)行時(shí)內(nèi)部實(shí)現(xiàn)的,涉及了內(nèi)存分配、數(shù)據(jù)拷貝和容量調(diào)整。擴(kuò)容的實(shí)現(xiàn)主要體現(xiàn)在 runtime.growslice
函數(shù)中。下面我們將深入分析 Go 切片的擴(kuò)容機(jī)制,結(jié)合源碼來進(jìn)行詳細(xì)解答。
1. 切片擴(kuò)容的觸發(fā)
切片是 Go 中的一種動(dòng)態(tài)數(shù)組,它有 長(zhǎng)度(len) 和 容量(cap)。當(dāng)我們向切片添加元素時(shí),切片的長(zhǎng)度會(huì)增加。如果長(zhǎng)度超出了當(dāng)前的容量,Go 會(huì)自動(dòng)擴(kuò)容。切片擴(kuò)容的過程是通過 append
函數(shù)來觸發(fā)的。
append 函數(shù)的實(shí)現(xiàn)
append
函數(shù)是 Go 內(nèi)置的函數(shù),用于向切片追加元素。它的實(shí)現(xiàn)會(huì)首先檢查當(dāng)前切片的容量是否足夠容納新的元素,如果不夠,它會(huì)調(diào)用 runtime.growslice
函數(shù)來擴(kuò)容。
// append() 源碼簡(jiǎn)化版 func append(slice []T, elems ...T) []T { // 1. 檢查當(dāng)前切片是否有足夠的容量 if len(slice)+len(elems) <= cap(slice) { // 如果容量足夠,直接追加元素 slice = slice[:len(slice)+len(elems)] copy(slice[len(slice)-len(elems):], elems) return slice } // 2. 如果容量不足,進(jìn)行擴(kuò)容 return growslice(reflect.TypeOf(slice).Elem(), slice, len(slice)+len(elems)) }
2. runtime.growslice 函數(shù)
當(dāng)切片容量不足時(shí),append
會(huì)調(diào)用 growslice
函數(shù)來擴(kuò)容。growslice
函數(shù)是 Go 運(yùn)行時(shí)內(nèi)部函數(shù),負(fù)責(zé)實(shí)際的內(nèi)存分配和切片擴(kuò)容操作。
growslice 函數(shù)源碼
// growslice 擴(kuò)容的實(shí)現(xiàn) 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} } // 擴(kuò)容策略:根據(jù)當(dāng)前容量和預(yù)期的新容量來決定新容量 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 } } } // 根據(jù)擴(kuò)容后的容量計(jì)算所需的內(nèi)存空間 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) } // 分配新的內(nèi)存,并拷貝舊的數(shù)據(jù) 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. 擴(kuò)容的具體過程
檢查容量是否足夠:
growslice
首先會(huì)檢查請(qǐng)求的擴(kuò)容是否超過了當(dāng)前切片的容量。如果新容量大于原容量的兩倍,它會(huì)直接使用新容量作為目標(biāo)容量。根據(jù)擴(kuò)容策略計(jì)算新容量:
如果切片的容量小于 256,它會(huì)直接翻倍容量(
newcap = cap
)。如果切片容量較大,擴(kuò)容時(shí)會(huì)采取逐步增加的策略:按原容量的 1.25 倍+192 (也就是3*threshold / 4) 增長(zhǎng),直到滿足目標(biāo)容量。
計(jì)算內(nèi)存所需大?。?nbsp;
growslice
會(huì)計(jì)算新切片所需的內(nèi)存大小,并分配新的內(nèi)存空間。對(duì)于不同類型的元素(如基本類型、指針類型等),內(nèi)存分配的方式會(huì)有所不同。內(nèi)存分配和數(shù)據(jù)拷貝:
通過
mallocgc
分配新的內(nèi)存區(qū)域,在分配空間的時(shí)候因根據(jù)標(biāo)簽的大小等級(jí)會(huì)向上取整,go在擴(kuò)容的時(shí)候?qū)幵笭奚徊糠謨?nèi)部碎片也要保證外部盡可能少的外部碎片,并使用memmove
將原切片的數(shù)據(jù)拷貝到新的內(nèi)存中。擴(kuò)容后的切片會(huì)更新 指針(指向新的內(nèi)存)、長(zhǎng)度 和 容量,然后返回新的切片。
4. 擴(kuò)容的性能考慮
切片擴(kuò)容的過程需要進(jìn)行 內(nèi)存分配 和 數(shù)據(jù)拷貝,這會(huì)影響性能,尤其是在頻繁擴(kuò)容的場(chǎng)景中。
頻繁擴(kuò)容的開銷:每次擴(kuò)容時(shí),都會(huì)重新分配內(nèi)存并將數(shù)據(jù)拷貝到新的內(nèi)存區(qū)域,這會(huì)帶來額外的時(shí)間和空間開銷。
容量預(yù)估:為了避免頻繁擴(kuò)容,最好在創(chuàng)建切片時(shí)預(yù)估切片需要的容量,尤其是在元素?cái)?shù)量已知的情況下。可以使用
make([]T, 0, n)
來預(yù)分配一個(gè)足夠的容量,以減少擴(kuò)容操作。
5. 擴(kuò)容策略的總結(jié)
小容量時(shí)(比如容量小于 256),切片會(huì)按 2 倍擴(kuò)容。
大容量時(shí),Go 會(huì)使用 1.25 倍擴(kuò)容 + 192 的策略,確保不會(huì)過度浪費(fèi)內(nèi)存。
對(duì)于
nil
或零容量切片,growslice
會(huì)初始化并分配合適的容量。
6. 示例:切片擴(kuò)容過程
下面是一個(gè)示例,展示了切片擴(kuò)容的過程:
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]
如上所示,切片的容量在每次添加元素時(shí)會(huì)根據(jù)擴(kuò)容策略進(jìn)行增長(zhǎng)。初始容量為 1,當(dāng)容量不足時(shí),擴(kuò)容后容量翻倍。最終,容量達(dá)到 16。
總結(jié)
Go 的切片擴(kuò)容機(jī)制是通過 append
函數(shù)和 growslice
函數(shù)實(shí)現(xiàn)的。在擴(kuò)容時(shí),Go 會(huì)根據(jù)當(dāng)前切片的容量和預(yù)期的容量進(jìn)行計(jì)算,并分配新的內(nèi)存空間,拷貝原有數(shù)據(jù)。擴(kuò)容的策略通常是小容量時(shí)翻倍,大容量時(shí)逐步增加,保證了內(nèi)存的高效使用。
到此這篇關(guān)于golang中slice擴(kuò)容的具體實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)golang slice擴(kuò)容內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go 簡(jiǎn)單實(shí)現(xiàn)多租戶數(shù)據(jù)庫(kù)隔離
本文主要介紹了Go 簡(jiǎn)單實(shí)現(xiàn)多租戶數(shù)據(jù)庫(kù)隔離,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05Makefile構(gòu)建Golang項(xiàng)目示例詳解
這篇文章主要為大家介紹了Makefile構(gòu)建Golang項(xiàng)目的過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07gorm golang 并發(fā)連接數(shù)據(jù)庫(kù)報(bào)錯(cuò)的解決方法
今天小編就為大家分享一篇gorm golang 并發(fā)連接數(shù)據(jù)庫(kù)報(bào)錯(cuò)的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-07-07go內(nèi)存隊(duì)列l(wèi)ist VS slice實(shí)現(xiàn)方式對(duì)比分析
這篇文章主要為大家介紹了go內(nèi)存隊(duì)列l(wèi)ist VS slice實(shí)現(xiàn)方式對(duì)比分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08Go語言的匿名字段實(shí)現(xiàn)組合復(fù)用實(shí)例探究
這篇文章主要為大家介紹了Go語言的匿名字段實(shí)現(xiàn)組合復(fù)用實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01golang調(diào)用shell命令(實(shí)時(shí)輸出,終止)
本文主要介紹了golang調(diào)用shell命令(實(shí)時(shí)輸出,終止),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02Go語言學(xué)習(xí)教程之goroutine和通道的示例詳解
這篇文章主要通過A?Tour?of?Go中的例子進(jìn)行學(xué)習(xí),以此了解Go語言中的goroutine和通道,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09Go?panic的三種產(chǎn)生方式細(xì)節(jié)探究
這篇文章主要介紹了Go?panic的三種產(chǎn)生方式細(xì)節(jié)探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12