您好,登錄后才能下訂單哦!
unsafe,顧名思義,是不安全的,Go定義這個包名也是這個意思,讓我們盡可能的不要使用它,如果你使用它,看到了這個名字,也會想到盡可能的不要使用它,或者更小心的使用它。
雖然這個包不安全,但是它也有它的優勢,那就是可以繞過Go的內存安全機制,直接對內存進行讀寫,所以有時候因為性能的需要,會冒一些風險使用該包,對內存進行操作。
Sizeof
函數可以返回一個類型所占用的內存大小,這個大小只有類型有關,和類型對應的變量存儲的內容大小無關,比如bool型占用一個字節、int8也占用一個字節。
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
返回一個類型的對齊值,也可以叫做對齊系數或者對齊倍數。對齊值是一個和內存對齊有關的值,合理的內存對齊可以提高內存讀寫的性能,關于內存對齊的知識可以參考相關文檔,這里不展開介紹。
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
基本上一樣。這里需要注意的是每個人的電腦運行的結果可能不一樣,大同小異。
func Alignof(x ArbitraryType) uintptr
此外,獲取對齊值還可以使用反射包的函數,也就是說:unsafe.Alignof(x)
等價于reflect.TypeOf(x).Align()
。
Offsetof
函數只適用于struct結構體中的字段相對于結構體的內存位置偏移量。結構體的第一個字段的偏移量都是0.
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有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字節,既不一樣,又不相同,這說明:
內存對齊影響struct的大小
struct的字段順序影響struct的大小
綜合以上兩點,我們可以得知,不同的字段順序,最終決定struct的內存大小,所以有時候合理的字段順序可以減少內存的開銷。
內存對齊會影響struct的內存占用大小,現在我們就詳細分析下,為什么字段定義的順序不同會導致struct的內存占用不一樣。
在分析之前,我們先看下內存對齊的規則:
對于具體類型來說,對齊值=min(編譯器默認對齊值,類型大小Sizeof長度)。也就是在默認設置的對齊值和類型的內存占用大小之間,取最小值為該類型的對齊值。我的電腦默認是8,所以最大值不會超過8.
struct在每個字段都內存對齊之后,其本身也要進行對齊,對齊值=min(默認對齊值,字段最大類型長度)。這條也很好理解,struct的所有字段中,最大的那個類型的長度以及默認對齊值之間,取最小的那個。
以上這兩條規則要好好理解,理解明白了才可以分析下面的struct結構體。在這里再次提醒,對齊值也叫對齊系數、對齊倍數,對齊模數。這就是說,每個字段在內存中的偏移量是對齊值的倍數即可。
我們知道byte,int32,int64的對齊值分別為1,4,8,占用內存大小也是1,4,8。那么對于第一個structuser1
,它的字段順序是byte、int32、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
以上給出了答案,推到過程大家可以參考user1
和user2
試試。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。