[譯]Go語言記憶體佈局

astaxie發表於2016-11-15

go語言記憶體佈局

在本文中,我將嘗試解釋Go如何在記憶體中構建結構體,以及結構體在位元組和位元位方面是什麼樣子。 希望我會成功,否則本文將是非常沉悶和混亂的。

想象一下,你有一個如下的結構體。

type MyData struct {
        aByte   byte
        aShort  int16
        anInt32 int32
        aSlice  []byte
}

那麼這個結構體究竟是什麼呢? 從根本上說,它描述瞭如何在記憶體中佈局資料。 這是什麼意思?編譯器又是如何展現出來呢? 我們來看一下。 首先讓我們使用反射來檢查結構中的欄位。

反射之上

下面是一些使用反射來找出欄位大小及其偏移量(它們相對於結構的開始位於記憶體中的位置)的程式碼。 反射可以告訴我們編譯器是怎麼看待型別(包括結構)的。

// First ask Go to give us some information about the MyData type
typ := reflect.TypeOf(MyData{})
fmt.Printf("Struct is %d bytes long\n", typ.Size())
// We can run through the fields in the structure in order
n := typ.NumField()
for i := 0; i < n; i++ {
        field := typ.Field(i)
        fmt.Printf("%s at offset %v, size=%d, align=%d\n",
            field.Name, field.Offset, field.Type.Size(), 
            field.Type.Align())
 }

除了每個欄位的偏移和大小,我還列印了每個欄位的對齊方式,我稍後會解釋。結果如下:

Struct is 32 bytes long
aByte at offset 0, size=1, align=1
aShort at offset 2, size=2, align=2
anInt32 at offset 4, size=4, align=4
aSlice at offset 8, size=24, align=8

aByte是我們結構體中的第一個欄位,偏移量為0.它使用1位元組的記憶體。

aShort是第二個欄位。它使用2位元組的記憶體。奇怪的是偏移量是2。這是為什麼呢?答案是對齊, CPU更好地訪問位於2位元組(“2位元組邊界”)的倍數的地址處的2個位元組,並訪問位於4位元組邊界上的4個位元組,直到CPU的自然整數大小,在現代CPU上是8位元組(64位)。

在一些較舊的RISC CPU訪問錯誤對齊的數字引起一個故障:在一些UNIX系統上,這將是一個SIGBUS,它會停止你的程式(或核心)。一些系統能夠處理這些錯誤並修復錯誤:您的程式碼將執行,但會緩慢的執行,因為額外的程式碼將由作業系統執行以修復錯誤。我相信英特爾和ARM的CPU也只是處理晶片上的任何不對齊:也許我們將在以後的文章中測試這一點,以及任何效能的影響。

無論如何,對齊是Go編譯器跳過一個位元組放置欄位aShort以便它位於2位元組邊界的原因。因為這樣,我們可以將另一個欄位放進結構體中,而不使它佔用更大記憶體。這裡是我們的結構的新版本,在aByte之後立即有一個新欄位anotherByte。

type MyData struct {
       aByte       byte
       anotherByte byte
       aShort      int16
       anInt32     int32
       aSlice      []byte
}

我們再次執行反射程式碼,可以看到anotherByte正好在aByte和aShort之間的空閒空間。 它坐落在偏移1,aShort仍然在偏移2.現在可能是時候注意我之前提到的那個神祕對齊欄位。 它告訴我們和Go編譯器,這個欄位需要如何對齊。

Struct is 32 bytes long
aByte at offset 0, size=1, align=1
anotherByte at offset 1, size=1, align=1
aShort at offset 2, size=2, align=2
anInt32 at offset 4, size=4, align=4
aSlice at offset 8, size=24, align=8

讓我看看記憶體

然而我們的結構體在記憶體中到底是什麼樣子? 讓我們看看我們能不能找到答案。 首先讓我們構建一個MyData例項,並填充一些值。我選擇了應該容易在記憶體中找到的值。

data := MyData{
        aByte:   0x1,
        aShort:  0x0203,
        anInt32: 0x04050607,
        aSlice:  []byte{
                0x08, 0x09, 0x0a,
        },
 }

現在一些程式碼訪問組成這個結構的位元組。 我們想要獲取這個結構的例項,在記憶體中找到它的地址,並列印出該記憶體中的位元組。 我們使用unsafe包來幫助我們這樣做。 這讓我們繞過Go型別系統將指向我們的結構的指標轉換為32位元組陣列,這個陣列就是組成我們的結構體的記憶體資料。

dataBytes := (*[32]byte)(unsafe.Pointer(&data))
fmt.Printf("Bytes are %#v\n", dataBytes)

我們執行以上程式碼。 這是結果,第一個欄位,aByte,從我們的結構中以粗體顯示。 這是希望你期望的,單位元組aByte = 0x01在偏移0。

Bytes are &[32]uint8{**0x1**, 0x0, 0x3, 0x2, 0x7, 0x6, 0x5, 0x4, 0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}

接下來我們來看看AShort。 這是在偏移量2的位置並且長度為2.如果你記得,aShort = 0x0203,但資料顯示的位元組是倒序。 這是因為大多數現代CPU都是Little-Endian:該值的最低位位元組首先出現在記憶體中。

Bytes are &[32]uint8{0x1, 0x0, **0x3, 0x2**, 0x7, 0x6, 0x5, 0x4, 0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}

同樣的事情發生在Int32 = 0x04050607。 最低位位元組首先出現在記憶體中。

Bytes are &[32]uint8{0x1, 0x0, 0x3, 0x2, **0x7, 0x6, 0x5, 0x4**, 0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}

神祕的插曲

現在我們看到什麼? 這是aSlice = [] byte {0x08,0x09,0x0a},在偏移量8的24個位元組。我沒有看到我的序列0x08,0x09,0x0a的任何地方的任何符號。 這是怎麼回事?

Bytes are &[32]uint8{0x1, 0x0, 0x3, 0x2, 0x7, 0x6, 0x5, 0x4, **0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0**, **0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0**}

Go反射包裡自有答案。 slice在Go語言中由以下結構體表示,該結構從指標資料開始,該資料指向儲存切片中的資料的儲存器; 然後是該儲存器中的有用資料的長度Len,以及該儲存器的大小Cap。

type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

如果把它提供給我們的程式碼,我們得到以下偏移和大小。 資料指標和兩個長度各為8個位元組,具有8個位元組對齊。

Struct is 24 bytes long
Data at offset 0, size=8, align=8
Len at offset 8, size=8, align=8
Cap at offset 16, size=8, align=8

如果我們再看一下後面的記憶體結構,我們可以看到資料是在地址0x000000c42001055a。 之後,我們看到Len和Cap都是3,這是我們的資料的長度。

Bytes are &[32]uint8{0x1, 0x0, 0x3, 0x2, 0x7, 0x6, 0x5, 0x4, **0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0**, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}

我們可以直接用以下程式碼訪問這些資料位元組。 首先讓我們直接訪問slice頭,然後列印出資料指向的記憶體。

dataslice := *(*reflect.SliceHeader)(unsafe.Pointer(&data.aSlice))
fmt.Printf("Slice data is %#v\n", 
        (*[3]byte)(unsafe.Pointer(dataslice.Data)))

這是輸出:

Slice data is &[3]uint8{0x8, 0x9, 0xa}

相關文章