前言
用過go語(yǔ)言的親們都知道悬槽,slice(中文翻譯為切片)在編程中經(jīng)常用到踊谋,它代表變長(zhǎng)的序列立由,序列中每個(gè)元素都有相同的類型筝家,類似一個(gè)動(dòng)態(tài)數(shù)組洼裤,利用append可以實(shí)現(xiàn)動(dòng)態(tài)增長(zhǎng),利用slice的特性可以很容易的切割slice溪王,它們是怎么實(shí)現(xiàn)這些特性的呢腮鞍?現(xiàn)在我們來探究一下這些特性的本質(zhì)是什么。
先了解一下slice的特性
定義一個(gè)slice:
s := []int{1,2,3,4,5}
fmt.Println(s) // [1 2 3 4 5]
一個(gè)slice類型一般寫作[]T莹菱,其中T代表slice中元素的類型移国;slice的語(yǔ)法和數(shù)組很像,只是沒有固定長(zhǎng)度而已道伟。
slice的擴(kuò)容:
s := []int{1,2,3,4,5}
s = append(s, 6)
fmt.Println(s) // [1 2 3 4 5 6]
內(nèi)置append函數(shù)在現(xiàn)有數(shù)組的長(zhǎng)度 < 1024 時(shí) cap 增長(zhǎng)是翻倍的迹缀,再往上的增長(zhǎng)率則是 1.25,至于為何后面會(huì)說蜜徽。
slice的切割:
s := []int{1,2,3,4,5,6}
s1 := s[0:2]
fmt.Println(s1) // [1 2]
s2 := s[4:]
fmt.Println(s2) // [5 6]
s3 := s[:4]
fmt.Println(s3) // [1 2 3 4]
slice作為函數(shù)參數(shù):
package main
import "fmt"
func main() {
slice_1 := []int{1, 2, 3, 4, 5}
fmt.Printf("main-->data:\t%#v\n", slice_1)
fmt.Printf("main-->len:\t%#v\n", len(slice_1))
fmt.Printf("main-->cap:\t%#v\n", cap(slice_1))
test1(slice_1)
fmt.Printf("main-->data:\t%#v\n", slice_1)
test2(&slice_1)
fmt.Printf("main-->data:\t%#v\n", slice_1)
}
func test1(slice_2 []int) {
slice_2[1] = 6666 // 函數(shù)外的slice確實(shí)有被修改
slice_2 = append(slice_2, 8888) // 函數(shù)外的不變
fmt.Printf("test1-->data:\t%#v\n", slice_2)
fmt.Printf("test1-->len:\t%#v\n", len(slice_2))
fmt.Printf("test1-->cap:\t%#v\n", cap(slice_2))
}
func test2(slice_2 *[]int) { // 這樣才能修改函數(shù)外的slice
*slice_2 = append(*slice_2, 6666)
}
結(jié)果:
main-->data: []int{1, 2, 3, 4, 5}
main-->len: 5
main-->cap: 5
test1-->data: []int{1, 6666, 3, 4, 5, 8888}
test1-->len: 6
test1-->cap: 12
main-->data: []int{1, 6666, 3, 4, 5}
main-->data: []int{1, 6666, 3, 4, 5, 6666}
這里要注意注釋的地方祝懂,為何slice作為值傳遞參數(shù),函數(shù)外的slice也被更改了拘鞋?為何在函數(shù)內(nèi)append不能改變函數(shù)外的slice砚蓬?要回答這些問題就得了解slice內(nèi)部結(jié)構(gòu),詳細(xì)請(qǐng)看下面.
slice的內(nèi)部結(jié)構(gòu)
其實(shí)slice在Go的運(yùn)行時(shí)庫(kù)中就是一個(gè)C語(yǔ)言動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)盆色,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定義:
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
這個(gè)結(jié)構(gòu)有3個(gè)字段怜械,第一個(gè)字段表示array的指針颅和,就是真實(shí)數(shù)據(jù)的指針(這個(gè)一定要注意),所以才經(jīng)常說slice是數(shù)組的引用缕允,第二個(gè)是表示slice的長(zhǎng)度峡扩,第三個(gè)是表示slice的容量,注意:len和cap都不是指針障本。
現(xiàn)在就可以解釋前面的例子slice作為函數(shù)參數(shù)提出的問題:
函數(shù)外的slice叫slice_1教届,函數(shù)的參數(shù)叫slice_2,當(dāng)函數(shù)傳遞slice_1的時(shí)候驾霜,其實(shí)傳入的確實(shí)是slice_1參數(shù)的復(fù)制案训,所以slice_2復(fù)制了slise_1,但要注意的是slice_2里存儲(chǔ)的數(shù)組的指針粪糙,所以當(dāng)在函數(shù)內(nèi)更改數(shù)組內(nèi)容時(shí)强霎,函數(shù)外的slice_1的內(nèi)容也改變了。在函數(shù)內(nèi)用append時(shí)蓉冈,append會(huì)自動(dòng)以倍增的方式擴(kuò)展slice_2的容量城舞,但是擴(kuò)展也僅僅是函數(shù)內(nèi)slice_2的長(zhǎng)度和容量,slice_1的長(zhǎng)度和容量是沒變的寞酿,所以在函數(shù)外打印時(shí)看起來就是沒變家夺。
append的運(yùn)作機(jī)制
在對(duì)slice進(jìn)行append等操作時(shí),可能會(huì)造成slice的自動(dòng)擴(kuò)容伐弹。其擴(kuò)容時(shí)的大小增長(zhǎng)規(guī)則是:
如果新的slice大小是當(dāng)前大小2倍以上拉馋,則大小增長(zhǎng)為新大小
否則循環(huán)以下操作:如果當(dāng)前slice大小小于1024,按每次2倍增長(zhǎng)惨好,否則每次按當(dāng)前大小1/4增長(zhǎng)煌茴。直到增長(zhǎng)的大小超過或等于新大小。
append的實(shí)現(xiàn)只是簡(jiǎn)單的在內(nèi)存中將舊slice復(fù)制給新slice
至于為何會(huì)這樣日川,你要看一下golang的源碼slice就知道了:
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
}
}
}
為何不用動(dòng)態(tài)鏈表實(shí)現(xiàn)slice蔓腐?
首先拷貝一斷連續(xù)的內(nèi)存是很快的,假如不想發(fā)生拷貝逗鸣,也就是用動(dòng)態(tài)鏈表,那你就沒有連續(xù)內(nèi)存绰精。此時(shí)隨機(jī)訪問開銷會(huì)是:鏈表 O(N), 2倍增長(zhǎng)塊鏈 O(LogN),二級(jí)表一個(gè)常數(shù)很大的O(1)撒璧。問題不僅是算法上開銷,還有內(nèi)存位置分散而對(duì)緩存高度不友好笨使,這些問題i在連續(xù)內(nèi)存方案里都是不存在的卿樱。除非你的應(yīng)用是狂append然后只順序讀一次,否則優(yōu)化寫而犧牲讀都完全不 make sense. 而就算你的應(yīng)用是嚴(yán)格順序讀硫椰,緩存命中率也通常會(huì)讓你的綜合效率比拷貝換連續(xù)內(nèi)存低繁调。
對(duì)小 slice 來說萨蚕,連續(xù) append 的開銷更多的不是在 memmove, 而是在分配一塊新空間的 memory allocator 和之后的 gc 壓力(這方面對(duì)鏈表更是不利)。所以蹄胰,當(dāng)你能大致知道所需的最大空間(在大部分時(shí)候都是的)時(shí)岳遥,在make的時(shí)候預(yù)留相應(yīng)的 cap 就好。如果所需的最大空間很大而每次使用的空間量分布不確定裕寨,那你就要在浪費(fèi)內(nèi)存和耗 CPU 在 allocator + gc 上做權(quán)衡浩蓉。
Go 在 append 和 copy 方面的開銷是可預(yù)知+可控的,應(yīng)用上簡(jiǎn)單的調(diào)優(yōu)有很好的效果宾袜。這個(gè)世界上沒有免費(fèi)的動(dòng)態(tài)增長(zhǎng)內(nèi)存捻艳,各種實(shí)現(xiàn)方案都有設(shè)計(jì)權(quán)衡。
什么時(shí)候該用slice庆猫?
在go語(yǔ)言中slice是很靈活的认轨,大部分情況都能表現(xiàn)的很好,但也有特殊情況月培。
當(dāng)程序要求slice的容量超大并且需要頻繁的更改slice的內(nèi)容時(shí)嘁字,就不應(yīng)該用slice,改用list更合適节视。
轉(zhuǎn)自: https://segmentfault.com/a/1190000005812839
后記
- slice的底層是array拳锚,make([]T, len, cap)的動(dòng)作是: 1. 初始化一個(gè)array;2. 返回一個(gè)指向1的slice
- slice的數(shù)據(jù)結(jié)構(gòu)中ptr至關(guān)重要寻行,它影響slice1 = slice2賦值過程霍掺,二者的底層數(shù)據(jù)結(jié)構(gòu)是同一份數(shù)據(jù)的拷貝(包含*ptr, len, cap)
- 其中*ptr相同,意味著不同slice最終指向的array元素是相同的地址拌蜘。
- 舉例: slice2 = slice1[2:]杆烁,其中&slice1[2] == &slice2[0]
- append函數(shù)是copy了連續(xù)內(nèi)存中的值到另一段連續(xù)內(nèi)存
- 舉例: slice2 = append(slice1, 3), 其中 &slice1[0] != &slice2[0]
- 以上的舉例不完全正確~~