golang slice 和 string 重用

相比於 c/c++,golang 的一個很大的改進就是引入了 gc 機制,再也不須要用戶本身管理內存,大大減小了程序因爲內存泄露而引入的 bug,可是同時 gc 也帶來了額外的性能開銷,有時甚至會由於使用不當,致使 gc 成爲性能瓶頸,因此 golang 程序設計的時候,應特別注意對象的重用,以減小 gc 的壓力。而 slice 和 string 是 golang 的基本類型,瞭解這些基本類型的內部機制,有助於咱們更好地重用這些對象c++

slice 和 string 內部結構

slice 和 string 的內部結構能夠在 $GOROOT/src/reflect/value.go 裏面找到git

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

能夠看到一個 string 包含一個數據指針和一個長度,長度是不可變的github

slice 包含一個數據指針、一個長度和一個容量,當容量不夠時會從新申請新的內存,Data 指針將指向新的地址,原來的地址空間將被釋放golang

從這些結構就能夠看出,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) 預分配內存,不知道長度的狀況下,能夠考慮下面的內存重用的方法app

內存重用

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 並不相同ui

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 得到的字符串),原本就不會申請額外的空間,這麼作簡直就是畫蛇添足

參考連接

轉載請註明出處
本文連接:http://hatlonely.com/2018/03/17/golang-slice-%E5%92%8C-string-%E9%87%8D%E7%94%A8/

相關文章
相關標籤/搜索