golang slice 和 string 重用
相比於 c/c++,golang 的一個很大的改進就是引入了 gc 機制,不再需要使用者自己管理記憶體,大大減少了程式由於記憶體洩露而引入的 bug,但是同時 gc 也帶來了額外的效能開銷,有時甚至會因為使用不當,導致 gc 成為效能瓶頸,所以 golang 程式設計的時候,應特別注意物件的重用,以減少 gc 的壓力。而 slice 和 string 是 golang 的基本型別,瞭解這些基本型別的內部機制,有助於我們更好地重用這些物件
slice 和 string 內部結構
slice 和 string 的內部結構可以在 $GOROOT/src/reflect/value.go
裡面找到
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
可以看到一個 string 包含一個資料指標和一個長度,長度是不可變的
slice 包含一個資料指標、一個長度和一個容量,當容量不夠時會重新申請新的記憶體,Data 指標將指向新的地址,原來的地址空間將被釋放
從這些結構就可以看出,string 和 slice 的賦值,包括當做引數傳遞,和自定義的結構體一樣,都僅僅是 Data 指標的淺拷貝
slice 重用
append 操作
si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
si2 := si1
si2 = append(si2, 0)
Convey("重新分配記憶體", func() {
header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
fmt.Println(header1.Data)
fmt.Println(header2.Data)
So(header1.Data, ShouldNotEqual, header2.Data)
})
si1 和 si2 開始都指向同一個陣列,當對 si2 執行 append 操作時,由於原來的 Cap 值不夠了,需要重新申請新的空間,因此 Data 值發生了變化,在 $GOROOT/src/reflect/value.go
這個檔案裡面還有關於新的 cap 值的策略,在 grow
這個函式裡面,當 cap 小於 1024 的時候,是成倍的增長,超過的時候,每次增長 25%,而這種記憶體增長不僅僅資料拷貝(從舊的地址拷貝到新的地址)需要消耗額外的效能,舊地址記憶體的釋放對 gc 也會造成額外的負擔,所以如果能夠知道資料的長度的情況下,儘量使用 make([]int, len, cap)
預分配記憶體,不知道長度的情況下,可以考慮下面的記憶體重用的方法
記憶體重用
si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
si2 := si1[:7]
Convey("不重新分配記憶體", func() {
header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
fmt.Println(header1.Data)
fmt.Println(header2.Data)
So(header1.Data, ShouldEqual, header2.Data)
})
Convey("往切片裡面 append 一個值", func() {
si2 = append(si2, 10)
Convey("改變了原 slice 的值", func() {
header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
fmt.Println(header1.Data)
fmt.Println(header2.Data)
So(header1.Data, ShouldEqual, header2.Data)
So(si1[7], ShouldEqual, 10)
})
})
si2 是 si1 的一個切片,從第一段程式碼可以看到切片並不重新分配記憶體,si2 和 si1 的 Data 指標指向同一片地址,而第二段程式碼可以看出,當我們往 si2 裡面 append 一個新的值的時候,我們發現仍然沒有記憶體分配,而且這個操作使得 si1 的值也發生了改變,因為兩者本就是指向同一片 Data 區域,利用這個特性,我們只需要讓 si1 = si1[:0]
就可以不斷地清空 si1 的內容,實現記憶體的複用了
PS: 你可以使用 copy(si2, si1)
實現深拷貝
string
Convey("字串常量", func() {
str1 := "hello world"
str2 := "hello world"
Convey("地址相同", func() {
header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
fmt.Println(header1.Data)
fmt.Println(header2.Data)
So(header1.Data, ShouldEqual, header2.Data)
})
})
這個例子比較簡單,字串常量使用的是同一片地址區域
Convey("相同字串的不同子串", func() {
str1 := "hello world"[:6]
str2 := "hello world"[:5]
Convey("地址相同", func() {
header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
fmt.Println(header1.Data, str1)
fmt.Println(header2.Data, str2)
So(str1, ShouldNotEqual, str2)
So(header1.Data, ShouldEqual, header2.Data)
})
})
相同字串的不同子串,不會額外申請新的記憶體,但是要注意的是這裡的相同字串,指的是 str1.Data == str2.Data && str1.Len == str2.Len
,而不是 str1 == str2
,下面這個例子可以說明 str1 == str2
但是其 Data 並不相同
Convey("不同字串的相同子串", func() {
str1 := "hello world"[:5]
str2 := "hello golang"[:5]
Convey("地址不同", func() {
header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
fmt.Println(header1.Data, str1)
fmt.Println(header2.Data, str2)
So(str1, ShouldEqual, str2)
So(header1.Data, ShouldNotEqual, header2.Data)
})
})
實際上對於字串,你只需要記住一點,字串是不可變的,任何字串的操作都不會申請額外的記憶體(對於僅內部資料指標而言),我曾自作聰明地設計了一個 cache 去儲存字串,以減少重複字串所佔用的空間,事實上,除非這個字串本身就是由 []byte
建立而來,否則,這個字串本身就是另一個字串的子串(比如通過 strings.Split
獲得的字串),本來就不會申請額外的空間,這麼做簡直就是多此一舉
參考連結
- Go Slices: usage and internals:<https://blog.golang.org/go-slices-usage-and-internals>
- 測試程式碼連結:<https://github.com/hatlonely/hellogolang/blob/master/internal/buildin/reuse_test.go>
> 轉載請註明出處 > 本文連結:<http://hatlonely.com/2018/03/17/golang-slice-%E5%92%8C-string-%E9%87%8D%E7%94%A8/>
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Golang 陣列和切片 Slice 和 Map 使用Golang陣列
- Golang Slice技巧Golang
- js原生api之String的slice方法JSAPI
- golang 之slice 變長陣列Golang陣列
- Golang slice 從原始碼來理解Golang原始碼
- golang []byte和string的高效能轉換Golang
- go(golang)之slice的小想法1Golang
- [golang]slice的坑:從append到共享GolangAPP
- golang-切片slice的基本介紹Golang
- Golang 切片(Slice)底層原始碼閱讀Golang原始碼
- golang slice使用不慎導致的問題Golang
- Rust的Vector vs. Golang的Slice比較RustGolang
- golang使用sqlx報錯:unsupported type []interface {}, a slice of interfaceGolangSQL
- go map 和 sliceGo
- Golang 陣列和字串之間的相互轉換[]byte/stringGolang陣列字串
- golang slice相關常見的效能最佳化手段Golang
- 【Go】深入剖析 slice 和 arrayGo
- 【Go】深入剖析slice和arrayGo
- javascript中string物件方法中的slice、substring、substr的區別聯絡JavaScript物件
- 小白學習Golang(七)Go語言StringGolang
- 區分slice,splice和split方法
- String s = “hello“和String s = new String(“hello“)的區別
- 重用其他程式庫
- 在Golang中使用泛型從任何map中獲取鍵的sliceGolang泛型
- JavaScript陣列裡的slice和spliceJavaScript陣列
- 【譯】Rust中的array、vector和sliceRust
- Go slice切片的“陷阱”和本質Go
- Golang工具集-String工具,時間工具,http工具等GolangHTTP
- String、StringBuilder和StringBufferUI
- Set<String>和JTextField
- 整明白 Golang slice 宣告方式、淺複製現象、深複製、append操作GolangAPP
- golang的fmt包String(),Error(),Format(),GoString()的介面實現GolangErrorORM
- 吐槽Javascript系列一:slice()、substr()和 substring()JavaScript
- go slice深拷貝和淺拷貝Go
- go slice深複製和淺複製Go
- 從重複到重用
- [20180702]物件名重用.txt物件
- String 和Inputstreem互轉