一文帶你讀懂結構體內存分配

一個博客引起的血案

一個比較牛逼的博客,介紹了如何優化字符串到字節數組的過程,避免了數據複製過程對程序性能的影響。git

對此,我深感佩服。由於代碼很是簡單,簡單到我根本看不懂!github

package main
 
import (
    "fmt"
    "strings"
    "unsafe"
)
 
func str2bytes(s string) []byte {
    x := (*[2]uintptr)(unsafe.Pointer(&s))
    h := [3]uintptr{x[0], x[1], x[1]}
    return *(*[]byte)(unsafe.Pointer(&h))
}
 
func bytes2str(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}
 
func main() {
    s := strings.Repeat("abc", 3)
    b := str2bytes(s)
    s2 := bytes2str(b)
    fmt.Println(b, s2)
}
複製代碼

之因此這麼作的緣由是:從 ptype 輸出的結構來看,string 可看作 [2]uintptr,而 []byte 則是 [3]uintptr,這便於咱們編寫代碼,無需額外定義結構類型。如此,str2bytes 只需構建 [3]uintptr{ptr, len, len},而 bytes2str 更簡單,直接轉換指針類型,忽略掉 cap 便可。golang

關於string[]byte結構能夠看下圖,我直接複製過來了,你們能夠看看上述表達的根據。segmentfault

打擊

做者的判斷呢,我是相信的。因而,我躍躍欲試的修改了代碼,也想完成相似的不用複製變量、僅僅修改指針類型就能夠的改變變量類型的過程。下面的代碼主要想作的就是,經過修改指針從而完成變量由結構體Num到結構體ReverseNum的轉變,代碼內容以下:數組

type Num struct {
    name  int8
    value int8
}

type ReverseNum struct {
    value int8
    name  int8
}
func main() {
    n := Num{100, 10}
    z := (*[2]uintptr)(unsafe.Pointer(&n))
    h := [2]uintptr{z[1], z[0]}
    fmt.Println(*(*ReverseNum)(unsafe.Pointer(&h))) // print result is {0, 0}
}
複製代碼

可是,結果並不如我所願。由於打印的結果並非{10, 100},而是{0, 0}。個人自信心受到了挫折,這種轉化究竟是什麼意思呢???安全

在反覆思考沒有結果以後,我在著名的stackoverflow貼出了個人疑問。而後就在我打算休息會兒的時候,就有人評論了,給予我深深的打擊。函數

對於個人這種寫法,人家列出了七點見解。在我緩了緩挫敗的心裏以後再看的時候,被人刪除了六點,惟一剩下的一點就是由於unsafe不夠安全。總的來講我就是在我對go不夠熟悉的時候,不要接觸或者使用unsafe包。我就在想,什麼知識不都是從不熟悉到熟悉的?我就是不夠熟悉因此纔會在stackoverflow上提問,也就是不熟悉纔想熟悉這個知識點而且嘗試熟悉這個知識點的啊!性能

unsafe確實不安全,可是並不妨礙我瞭解這個包啊。優化

而後我就放棄了,畢竟評論的都是大佬,我這種渣渣也許就真的不適合知道這種知識。ui

起色

機緣巧合之下,我又看到了一篇博客,是介紹內存對齊的。其實以前也是看過內存對齊的文章,只不過僅僅是瞭解下。這篇文章讓我想起了以前的疑問,因此我就帶着疑問來反覆讀的這篇博客。

獲得的知識和以前看內存對齊的博客是一致的,只不過此次我有了新的感悟。結構體的內存分配確定是和內存對齊相關的。爲了獲得內存對齊的展現效果,此次沒有使用兩個都是int8屬性的結構體。而是使用了一個新的結構體Student,有兩個屬性,一個屬性是int8,另一個是int64

import (
	"fmt"
	"unsafe"
)

type Student struct {
	age    int8
	salary int64
}

type StudentReverse struct {
	salary int64
	age    int8
}

func main() {
	s := Student{18, 100000}
	x := (*[2]uintptr)(unsafe.Pointer(&s))
	fmt.Println("age is ", *(*int8)(unsafe.Pointer(&x[0])))// 18
}
複製代碼

這樣打印的結果就是我想要的了,和我在Student初始化的時候賦值一致。而後須要作的就是如何經過指針修改類型了,既然第一步作到了,那麼第二步就簡單了,根據大佬的博客照葫蘆畫瓢就行了。

tmp := [2]uintptr{x[1], x[0]}
	studentReverse := *(*StudentReverse)(unsafe.Pointer(&tmp))
	fmt.Println(studentReverse.salary, studentReverse.age)
複製代碼

打印的結果和預期一致,新的studentReverse結構體變量就按照預期進行告終構體的變換。

可是這種作法沒什麼意義,由於uintptr其實就是一個通用的指針,在函數str2bytes中的用法比較trick,不只僅把結構體string中的數組指針做爲指針,還把底層數組的長度也做爲了指針。而在把Student轉化爲StudentReverse的過程當中,只不過是把Student中每一個元素值複製了了一份,沒有任何意義。

讀者能夠嘗試下修改變量s的屬性,看下studentReverse是否也對應的修改了

還剩下最後一個問題,爲何貼在stackoverflow的代碼就沒有成功的運行。仍是由於內存對齊,這兩個int8類型的變量,由於內存對齊,放到了一個64位的內存中去了(要看系統支持的位數,個人電腦是64位的)。爲了驗證正確性呢,能夠看以下代碼

import (
	"fmt"
	"unsafe"
)

type Test struct {
	a int8
	b int8
}

func main() {
	test := Test{2, 3}
	z := (*[2]int8)(unsafe.Pointer(&test))
	fmt.Println("z is ", z)//z is &[2 3]
	fmt.Printf("totally as one result is %b\n", *(*int16)(unsafe.Pointer(&test)))//totally as one result is 1100000010
}

複製代碼

代碼運行的結果z中,就是一個長度爲2的數組指針,包含有兩個值,一個是2(也就是t.a的值),一個是3(也就是t.b的值)。若是把結構體轉化爲一個int16的變量並按照二進制進行打印,結果是1100000010,若是看的仔細的話,就知道後八位是2,前兩位是3。

總的來講就是每一個結構體地址後面有一段的內存空間,用戶存放此結構體的屬性。因此就有了unsafe包能夠操做地址,操做(*[2]int8)(unsafe.Pointer(&test))就是把變量test的地址以後的16位轉化爲了長度爲2的元素類型爲int8的數組,這樣就能夠直接經過操做指針的方式來操做內存。

可是呢,這些屬性由於內存對齊,並非一個一個緊湊而且連續排列的。而內存對齊在不一樣的操做系統或者不一樣的硬件上的要求也是各不相同。爲了不例如你在這個64位系統能夠正常運行的操做,到了32位系統就崩潰了,因此 go 就極其不建議使用unsafe包。

總結

雖然以前對於分配很疑惑,頗有挫敗感,可是如今也以爲沒什麼?除了開心,也好像沒有其餘的。任何知識都是一層窗戶紙,窗戶紙的兩邊就是兩個世界的人。可是你要是覺得你捅破了這層窗戶紙你就很厲害,那你錯了,由於兩個直接中間隔離的僅僅是一層窗戶紙。

相關文章
相關標籤/搜索