13.1. unsafe.Sizeof, Alignof 和 Offsetof

unsafe.Sizeof函數返回操作數在內存中的字節大小,參數可以是任意類型的表達式,但是它並不會對錶達式進行求值。一個Sizeof函數調用是一個對應uintptr類型的常量表達式,因此返回的結果可以用作數組類型的長度大小,或者用作計算其他的常量。

import "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"

Sizeof函數返回的大小隻包括數據結構中固定的部分,例如字符串對應結構體中的指針和字符串長度部分,但是並不包含指針指向的字符串的內容。Go語言中非聚合類型通常有一個固定的大小,儘管在不同工具鏈下生成的實際大小可能會有所不同。考慮到可移植性,引用類型或包含引用類型的大小在32位平臺上是4個字節,在64位平臺上是8個字節。

計算機在加載和保存數據時,如果內存地址合理地對齊的將會更有效率。例如2字節大小的int16類型的變量地址應該是偶數,一個4字節大小的rune類型變量的地址應該是4的倍數,一個8字節大小的float64、uint64或64-bit指針類型變量的地址應該是8字節對齊的。但是對於再大的地址對齊倍數則是不需要的,即使是complex128等較大的數據類型最多也只是8字節對齊。

由於地址對齊這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和,或者更大因為可能存在內存空洞。內存空洞是編譯器自動添加的沒有被使用的內存空間,用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊(譯註:內存空洞可能會存在一些隨機數據,可能會對用unsafe包直接操作內存的處理產生影響)。

類型 大小
bool 1個字節
intN, uintN, floatN, complexN N/8個字節(例如float64是8個字節)
int, uint, uintptr 1個機器字
*T 1個機器字
string 2個機器字(data、len)
[]T 3個機器字(data、len、cap)
map 1個機器字
func 1個機器字
chan 1個機器字
interface 2個機器字(type、value)

Go語言的規範並沒有要求一個字段的聲明順序和內存中的順序是一致的,所以理論上一個編譯器可以隨意地重新排列每個字段的內存位置,雖然在寫作本書的時候編譯器還沒有這麼做。下面的三個結構體雖然有著相同的字段,但是第一種寫法比另外的兩個需要多50%的內存。

                               // 64-bit  32-bit
struct{ bool; float64; int16 } // 3 words 4words
struct{ float64; int16; bool } // 2 words 3words
struct{ bool; int16; float64 } // 2 words 3words

關於內存地址對齊算法的細節超出了本書的範圍,也不是每一個結構體都需要擔心這個問題,不過有效的包裝可以使數據結構更加緊湊(譯註:未來的Go語言編譯器應該會默認優化結構體的順序,當然應該也能夠指定具體的內存佈局,相同討論請參考 Issue10014 ),內存使用率和性能都可能會受益。

unsafe.Alignof 函數返回對應參數的類型需要對齊的倍數。和 Sizeof 類似, Alignof 也是返回一個常量表達式,對應一個常量。通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節),其它的類型對齊到機器字大小。

unsafe.Offsetof 函數的參數必須是一個字段 x.f,然後返回 f 字段相對於 x 起始地址的偏移量,包括可能的空洞。

圖 13.1 顯示了一個結構體變量 x 以及其在32位和64位機器上的典型的內存。灰色區域是空洞。

var x struct {
    a bool
    b int16
    c []int
}

下面顯示了對x和它的三個字段調用unsafe包相關函數的計算結果:

32位系統:

Sizeof(x)   = 16  Alignof(x)   = 4
Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12  Alignof(x.c) = 4 Offsetof(x.c) = 4

64位系統:

Sizeof(x)   = 32  Alignof(x)   = 8
Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24  Alignof(x.c) = 8 Offsetof(x.c) = 8

雖然這幾個函數在不安全的unsafe包,但是這幾個函數調用並不是真的不安全,特別在需要優化內存空間時它們返回的結果對於理解原生的內存佈局很有幫助。