溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

SliceHeader和StringHeader有什么區別

發布時間:2021-06-16 10:54:55 來源:億速云 閱讀:245 作者:chen 欄目:編程語言

本篇內容介紹了“SliceHeader和StringHeader有什么區別”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

今天要給大家介紹的是 SliceHeader 和 StringHeader 結構體,了解清楚他到底是什么,又有什么用,并且會在最后給大家介紹 0  拷貝轉換的內容。

一起愉快地開始吸魚之路。

SliceHeader

SliceHeader 如其名,Slice + Header,看上去很直觀,實際上是 Go Slice(切片)的運行時表現。

type SliceHeader struct {  Data uintptr  Len  int  Cap  int }
  • Data:指向具體的底層數組。

  • Len:代表切片的長度。

  • Cap:代表切片的容量。

既然知道了切片的運行時表現,那是不是就意味著我們可以自己造一個?

在日常程序中,可以利用標準庫 reflect 提供的 SliceHeader 結構體造一個:

func main() {   // 初始化底層數組  s := [4]string{"腦子", "進", "煎魚", "了"}  s1 := s[0:1]  s2 := s[:]    // 構造 SliceHeader  sh2 := (*reflect.SliceHeader)(unsafe.Pointer(&s1))  sh3 := (*reflect.SliceHeader)(unsafe.Pointer(&s2))  fmt.Println(sh2.Len, sh2.Cap, sh2.Data)  fmt.Println(sh3.Len, sh3.Cap, sh3.Data) }

你認為輸出結果是什么,這兩個新切片會指向同一個底層數組的內存地址嗎?

輸出結果:

1 4 824634330936  4 4 824634330936

兩個切片的 Data 屬性所指向的底層數組是一致的,Len 屬性的值不一樣,sh2 和 sh3 分別是兩個切片。

疑問

為什么兩個新切片所指向的 Data 是同一個地址的呢?

這其實是 Go 語言本身為了減少內存占用,提高整體的性能才這么設計的。

將切片復制到任意函數的時候,對底層數組大小都不會影響。復制時只會復制切片本身(值傳遞),不會涉及底層數組。

也就是在函數間傳遞切片,其只拷貝 24 個字節(指針字段 8 個字節,長度和容量分別需要 8 個字節),效率很高。

這種設計也引出了新的問題,在平時通過 s[i:j] 所生成的新切片,兩個切片底層指向的是同一個底層數組。

假設在沒有超過容量(cap)的情況下,對第二個切片操作會影響第一個切片。

這是很多 Go 開發常會碰到的一個大 “坑”,不清楚的排查了很久的都不得而終。

StringHeader

除了 SliceHeader 外,Go 語言中還有一個典型代表,那就是字符串(string)的運行時表現。

type StringHeader struct {    Data uintptr    Len  int }
  • Data:存放指針,其指向具體的存儲數據的內存區域。

  • Len:字符串的長度。

可得知 “Hello” 字符串的底層數據如下:

var data = [...]byte{     'h', 'e', 'l', 'l', 'o', }

底層的存儲示意圖如下:

SliceHeader和StringHeader有什么區別

真實演示例子如下:

func main() {  s := "腦子進煎魚了"  s1 := "腦子進煎魚了"  s2 := "腦子進煎魚了"[7:]   fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)  fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data)  fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s2)).Data) }

你認為輸出結果是什么,變量 s 和 s1、s2 會指向同一個底層內存空間嗎?

輸出結果:

17608227  17608227  17608234

從輸出結果來看,變量 s 和 s1 指向同一個內存地址。變量 s2 雖稍有偏差,但本質上也是指向同一塊。

因為其是字符串的切片操作,是從第 7 位索引開始,因此正好的 17608234-17608227 =  7。也就是三個變量都是指向同一塊內存空間,這是為什么呢?

這是因為在 Go 語言中,字符串都是只讀的,為了節省內存,相同字面量的字符串通常對應于同一字符串常量,因此指向同一個底層數組。

0 拷貝轉換

為什么會有人關注到 SliceHeader、StringHeader 這類運行時細節呢,一大部分原因是業內會有開發者,希望利用其實現零拷貝的 string  到 bytes 的轉換。

常見轉換代碼如下:

func string2bytes(s string) []byte {  stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))   bh := reflect.SliceHeader{   Data: stringHeader.Data,   Len:  stringHeader.Len,   Cap:  stringHeader.Len,  }   return *(*[]byte)(unsafe.Pointer(&bh)) }

但這其實是錯誤的,官方明確表示:

the Data field is not sufficient to guarantee the data it references will not  be garbage collected, so programs must keep a separate, correctly typed pointer  to the underlying data.

SliceHeader、StringHeader 的 Data 字段是一個 uintptr 類型。由于 Go 語言只有值傳遞。

因此在上述代碼中會出現將 Data 作為值拷貝的情況,這就會導致無法保證它所引用的數據不會被垃圾回收(GC)。

應該使用如下轉換方式:

func main() {  s := "腦子進煎魚了"  v := string2bytes1(s)  fmt.Println(v) }  func string2bytes1(s string) []byte {  stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))   var b []byte  pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))  pbytes.Data = stringHeader.Data  pbytes.Len = stringHeader.Len  pbytes.Cap = stringHeader.Len   return b }

在程序必須保留一個單獨的、正確類型的指向底層數據的指針。

在性能方面,若只是期望單純的轉換,對容量(cap)等字段值不敏感,也可以使用以下方式:

func string2bytes2(s string) []byte {  return *(*[]byte)(unsafe.Pointer(&s)) }

性能對比:

string2bytes1-1000-4   3.746 ns/op  0 allocs/op string2bytes1-1000-4   3.713 ns/op  0 allocs/op string2bytes1-1000-4   3.969 ns/op  0 allocs/op  string2bytes2-1000-4   2.445 ns/op  0 allocs/op string2bytes2-1000-4   2.451 ns/op  0 allocs/op string2bytes2-1000-4   2.455 ns/op  0 allocs/op

會相當標準的轉換性能會稍快一些,這種強轉也會導致一個小問題。

代碼如下:

func main() {  s := "腦子進煎魚了"  v := string2bytes2(s)  println(len(v), cap(v)) } func string2bytes2(s string) []byte {  return *(*[]byte)(unsafe.Pointer(&s)) }

輸出結果:

18 824633927632

這種強轉其會導致 byte 的切片容量非常大,需要特別注意。一般還是推薦使用標準的 SliceHeader、StringHeader  方式就好了,也便于后來的維護者理解。

總結

在這篇文章中,我們介紹了字符串(string)和切片(slice)的兩個運行時表現,分別是 StringHeader 和 SliceHeader。

同時了解到其運行時表現后,我們還針對其兩者的地址指向,常見坑進行了說明。

最后我們進一步深入,面向 0 拷貝轉換的場景進行了介紹和性能分析。

參考

Go語言slice的本質-SliceHeader

數組、字符串和切片

零拷貝實現string 和bytes的轉換疑問

“SliceHeader和StringHeader有什么區別”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女