Go語言之 unsafe 包以內存佈局

unsafe,顧名思義,是不安全的,Go定義這個包名也是這個意思,讓咱們儘量的不要使用它,若是你使用它,看到了這個名字,也會想到儘量的不要使用它,或者更當心的使用它。express

雖然這個包不安全,可是它也有它的優點,那就是能夠繞過Go的內存安全機制,直接對內存進行讀寫,因此有時候由於性能的須要,會冒一些風險使用該包,對內存進行操做。安全

Sizeof函數

Sizeof函數能夠返回一個類型所佔用的內存大小,這個大小隻有類型有關,和類型對應的變量存儲的內容大小無關,好比bool型佔用一個字節、int8也佔用一個字節。ide

func main() {
    fmt.Println(unsafe.Sizeof(true))
    fmt.Println(unsafe.Sizeof(int8(0)))
    fmt.Println(unsafe.Sizeof(int16(10)))
    fmt.Println(unsafe.Sizeof(int32(10000000)))
    fmt.Println(unsafe.Sizeof(int64(10000000000000)))
    fmt.Println(unsafe.Sizeof(int(10000000000000000)))
}

對於整型來講,佔用的字節數意味着這個類型存儲數字範圍的大小,好比int8佔用一個字節,也就是8bit,因此它能夠存儲的大小範圍是-128~~127,也就是−2^(n-1)到2^(n-1)−1,n表示bit,int8表示8bit,int16表示16bit,其餘以此類推。函數

對於和平臺有關的int類型,這個要看平臺是32位仍是64位,會取最大的。好比我本身測試,以上輸出,會發現int和int64的大小是同樣的,由於個人是64位平臺的電腦。佈局

func Sizeof(x ArbitraryType) uintptr

以上是Sizeof的函數定義,它接收一個ArbitraryType類型的參數,返回一個uintptr類型的值。這裏的ArbitraryType不用關心,他只是一個佔位符,爲了文檔的考慮導出了該類型,可是通常不會使用它,咱們只須要知道它表示任何類型,也就是咱們這個函數能夠接收任意類型的數據。性能

// ArbitraryType is here for the purposes of documentation only and is not actually// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

Alignof 函數

Alignof返回一個類型的對齊值,也能夠叫作對齊係數或者對齊倍數。對齊值是一個和內存對齊有關的值,合理的內存對齊能夠提升內存讀寫的性能,關於內存對齊的知識能夠參考相關文檔,這裏不展開介紹。測試

func main() {
   var b bool    var i8 int8    var i16 int16    var i64 int64    var f32 float32    var s string    var m map[string]string    var p *int32    fmt.Println(unsafe.Alignof(b))    fmt.Println(unsafe.Alignof(i8))    fmt.Println(unsafe.Alignof(i16))    fmt.Println(unsafe.Alignof(i64))    fmt.Println(unsafe.Alignof(f32))    fmt.Println(unsafe.Alignof(s))    fmt.Println(unsafe.Alignof(m))    fmt.Println(unsafe.Alignof(p)) }

從以上例子的輸出,能夠看到,對齊值通常是2^n,最大不會超過8(緣由見下面的內存對齊規則)。Alignof的函數定義和Sizeof基本上同樣。這裏須要注意的是每一個人的電腦運行的結果可能不同,大同小異。ui

func Alignof(x ArbitraryType) uintptr

此外,獲取對齊值還可使用反射包的函數,也就是說:unsafe.Alignof(x)等價於reflect.TypeOf(x).Align()spa

Offsetof 函數

Offsetof函數只適用於struct結構體中的字段相對於結構體的內存位置偏移量。結構體的第一個字段的偏移量都是0.code

func main() {
   var u1 user1    fmt.Println(unsafe.Offsetof(u1.b))    fmt.Println(unsafe.Offsetof(u1.i))    fmt.Println(unsafe.Offsetof(u1.j)) }
type user1 struct {    b byte    i int32    j int64
}

字段的偏移量,就是該字段在struct結構體內存佈局中的起始位置(內存位置索引從0開始)。根據字段的偏移量,咱們能夠定位結構體的字段,進而能夠讀寫該結構體的字段,哪怕他們是私有的,***的感受有沒有。偏移量的概念,咱們會在下一小結詳細介紹。

此外,unsafe.Offsetof(u1.i)等價於reflect.TypeOf(u1).Field(i).Offset

有意思的struct大小

咱們定義一個struct,這個struct有3個字段,它們的類型有byte,int32以及int64,可是這三個字段的順序咱們能夠任意排列,那麼根據順序的不一樣,一共有6種組合。

type user1 struct {
    b byte
    i int32
    j int64
}
type user2 struct {    b byte    j int64    i int32
}
type user3 struct {    i int32    b byte    j int64
}
type user4 struct {    i int32    j int64    b byte
}
type user5 struct {    j int64    b byte    i int32
}
type user6 struct {    j int64    i int32    b byte
}

根據這6種組合,定義了6個struct,分別位user1,user2,…,user6,那麼如今你們猜想一下,這6種類型的struct佔用的內存是多少,就是unsafe.Sizeof()的值。

你們可能猜想1+4+8=13,由於byte的大小爲1,int32大小爲4,int64大小爲8,而struct其實就是一個字段的組合,因此猜想struct大小爲字段大小之和也很正常。

可是,可是,我能夠明確的說,這是錯誤的。

爲何是錯誤的,由於有內存對齊存在,編譯器使用了內存對齊,那麼最後的大小結果就不同了。如今咱們正式驗證下,這幾種struct的值。

func main() {
   var u1 user1    
   var u2 user2    
   var u3 user3    
   var u4 user4    
   var u5 user5    
   var u6 user6    fmt.Println("u1 size is ",unsafe.Sizeof(u1))    fmt.Println("u2 size is ",unsafe.Sizeof(u2))    fmt.Println("u3 size is ",unsafe.Sizeof(u3))    fmt.Println("u4 size is ",unsafe.Sizeof(u4))    fmt.Println("u5 size is ",unsafe.Sizeof(u5))    fmt.Println("u6 size is ",unsafe.Sizeof(u6)) }

從以上輸出能夠看到,結果是:

u1 size is  16
u2 size is  24
u3 size is  16
u4 size is  24
u5 size is  16
u6 size is  16

結果出來了(個人電腦的結果,Mac64位,你的可能不同),4個16字節,2個24字節,既不同,又不相同,這說明:

  1. 內存對齊影響struct的大小

  2. struct的字段順序影響struct的大小

綜合以上兩點,咱們能夠得知,不一樣的字段順序,最終決定struct的內存大小,因此有時候合理的字段順序能夠減小內存的開銷

內存對齊會影響struct的內存佔用大小,如今咱們就詳細分析下,爲何字段定義的順序不一樣會致使struct的內存佔用不同。

在分析以前,咱們先看下內存對齊的規則:

  1. 對於具體類型來講,對齊值=min(編譯器默認對齊值,類型大小Sizeof長度)。也就是在默認設置的對齊值和類型的內存佔用大小之間,取最小值爲該類型的對齊值。個人電腦默認是8,因此最大值不會超過8.

  2. struct在每一個字段都內存對齊以後,其自己也要進行對齊,對齊值=min(默認對齊值,字段最大類型長度)。這條也很好理解,struct的全部字段中,最大的那個類型的長度以及默認對齊值之間,取最小的那個。

以上這兩條規則要好好理解,理解明白了才能夠分析下面的struct結構體。在這裏再次提醒,對齊值也叫對齊係數、對齊倍數,對齊模數。這就是說,每一個字段在內存中的偏移量是對齊值的倍數便可

咱們知道byte,int32,int64的對齊值分別爲1,4,8,佔用內存大小也是1,4,8。那麼對於第一個structuser1,它的字段順序是byte、int3二、int64,咱們先使用第1條內存對齊規則進行內存對齊,其內存結構以下,內存佈局中有豎線(|),用於每四個字節的分割,下同。

bxxx|iiii|jjjj|jjjj

user1類型,第1個字段byte,對齊值1,大小1,因此放在內存佈局中的第1位。

第2個字段int32,對齊值4,大小4,因此它的內存偏移值必須是4的倍數,在當前的user1中,就不能從第2位開始了,必須從第5位開始,也就是偏移量爲4。第2,3,4位由編譯器進行填充,通常爲值0,也稱之爲內存空洞。因此第5位到第8位爲第2個字段i。

第3字段,對齊值爲8,大小也是8。由於user1前兩個字段已經排到了第8位,因此下一位的偏移量正好是8,是第3個字段對齊值的倍數,不用填充,能夠直接排列第3個字段,也就是從第9位到第16位爲第3個字段j。

如今第一條內存對齊規則後,內存長度已經爲16個字節,咱們開始使用內存的第2條規則進行對齊。根據第二條規則,默認對齊值8,字段中最大類型長度也是8,因此求出結構體的對齊值位8,咱們目前的內存長度爲16,是8的倍數,已經實現了對齊。

因此到此爲止,結構體user1的內存佔用大小爲16字節。

如今咱們再分析一個user2類型,它的大小是24,只是調換了一下字段i和j的順序,就多佔用了8個字節,咱們看看爲何?仍是先使用咱們的內存第1條規則分析。

bxxx|xxxx|jjjj|jjjj|iiii

按對齊值和其佔用的大小,第1個字段b偏移量爲0,佔用1個字節,放在第1位。

第2個字段j,是int64,對齊值和大小都是8,因此要從偏移量8開始,也就是第9到16位爲j,這也就意味着第2到8位被編譯器填充。

目前整個內存佈局已經偏移了16位,正好是第3個字段i的對齊值4的倍數,因此不用填充,能夠直接排列,第17到20位爲i。

如今全部字段對齊好了,整個內存大小爲1+7+8+4=20個字節,咱們開始使用內存對齊的第2條規則,也就是結構體的對齊,經過默認對齊值和最大的字段大小,求出結構體的對齊值爲8。

如今咱們的整個內存佈局大小爲20,不是8的倍數,因此咱們須要進行內存填充,補足到8的倍數,最小的就是24,因此對齊後整個內存佈局爲

bxxx|xxxx|jjjj|jjjj|iiii|xxxx

因此這也是爲何咱們最終得到的user2的大小爲24的緣由。
基於以上辦法,咱們能夠得出其餘幾個struct的內存佈局。

user3

iiii|bxxx|jjjj|jjjj

user4

iiii|xxxx|jjjj|jjjj|bxxx|xxxx

user5

jjjj|jjjj|bxxx|iiii

user6

jjjj|jjjj|iiii|bxxx

以上給出了答案,推到過程你們能夠參考user1user2試試。

相關文章
相關標籤/搜索