Go 內(nèi)存對齊-結(jié)構(gòu)體

無論什么語言,類型都涉及到了編程語法的方方面面。加強(qiáng)對于類型和指針的理解,對于提高編程水平十分關(guān)鍵。本文會(huì)主要講解類型。

關(guān)于為什么需要內(nèi)存對齊請看這里:
內(nèi)存管理-內(nèi)存對齊

我們首先來看看這幾個(gè)字節(jié)的內(nèi)存:

FFE4 FFE4 FFE4 FFE4
00000000 11001011 01100101 00001010

請問地址 FFE1 上字節(jié)的值是多少?如果你試圖回答一個(gè)結(jié)果,那就是錯(cuò)的。為什么?因?yàn)槲疫€沒有告訴你這個(gè)字節(jié)表示什么。我還沒有告訴你類型信息。

如果我說上述字節(jié)表示一個(gè)數(shù)字會(huì)怎么樣呢?你可能會(huì)回答 10,那么你又錯(cuò)了。為什么?因?yàn)楫?dāng)我說這是數(shù)字的時(shí)候,你認(rèn)為我是指十進(jìn)制的數(shù)字。

基數(shù)(number base):

所有編號(hào)系統(tǒng)(numbering system)要發(fā)揮作用,都要有一個(gè)基(base)。從你出生的時(shí)候開始,人們就教你用基數(shù) 10 來數(shù)數(shù)了。這可能是因?yàn)槲覀兇蠖鄶?shù)人都有 10 個(gè)手指和 10 個(gè)腳趾。另外,用基數(shù) 10 來進(jìn)行數(shù)學(xué)計(jì)算也很自然。

基定義了編號(hào)系統(tǒng)所包含的符號(hào)數(shù)。基數(shù) 10 會(huì)有 10 個(gè)不同的符號(hào),用以表示我們可以計(jì)量的無限事物?;鶖?shù) 10 的編號(hào)系統(tǒng)為 0、1、2、3、4、5、6、7、8、9。一旦超過了 9,我們需要增加數(shù)的長度。例如,10、100 和 1000。

在計(jì)算機(jī)領(lǐng)域,我們還一直使用其他兩種基。第一種是基數(shù) 2(或二進(jìn)制數(shù)),例如上圖所表示的位。第二種是基數(shù) 16(或十六進(jìn)制數(shù)),例如上圖中表示的地址。

在二進(jìn)制編號(hào)系統(tǒng)(基數(shù) 2)中,只有兩種符號(hào),即 0 和 1。

在十六進(jìn)制數(shù)字系統(tǒng)(基數(shù) 16)中,有 16 個(gè)符號(hào),這些符號(hào)分別是:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F。

如果桌上有些蘋果,那些蘋果可以用任何編號(hào)系統(tǒng)來表示。我們可以說這里有:

10010001 個(gè)蘋果(使用 2 作為基數(shù))
145 個(gè)蘋果(使用 10 作為基數(shù))
91 個(gè)蘋果(使用 16 作為基數(shù))
所有答案都正確,只要給定了正確的基。

注意每個(gè)編號(hào)系統(tǒng)表示那些蘋果所需要的符號(hào)數(shù)?;鶖?shù)越大,編號(hào)系統(tǒng)的效率就越高。

對于計(jì)算機(jī)地址、IP 地址和顏色代碼,使用 16 作為基數(shù),就顯得很有價(jià)值。

看看用三種基,來分別表示 HTML 的顏色(“白”)的數(shù)字:

使用 2 作為基數(shù):1111 1111 1111 1111 1111 1111(24 個(gè)字符)
使用 10 作為基數(shù):16777215(10 個(gè)字符)
使用 16 作為基數(shù):FFFFFF(6 個(gè)字符)
你會(huì)選擇哪個(gè)編號(hào)系統(tǒng)來表示顏色呢?

現(xiàn)在,如果我告訴你,地址 FFE1 處的字節(jié)表示一個(gè)基數(shù)為 10 的數(shù)字,你回答 10,這就正確了。

類型提供了兩條信息,你和編譯器都需要它來執(zhí)行我們剛剛經(jīng)歷過的練習(xí)。

要查看的內(nèi)存數(shù)量(以字節(jié)為單位)
這些字節(jié)的表示
Go 語言提供了以下基本數(shù)字類型:

無符號(hào)整數(shù)
uint8, uint16, uint32, uint64
有符號(hào)整數(shù)
int8, int16, int32, int64
實(shí)數(shù)
float32, float64
預(yù)聲明整數(shù)
uint, int, uintptr

這些關(guān)鍵字提供了所有的類型信息。

uint8 包含一個(gè)基為 10 的數(shù)字,用 1 個(gè)存儲(chǔ)字節(jié)表示。uint8 的值從 0 到 255。

int32 包含一個(gè)基為 10 的數(shù)字,用 4 個(gè)存儲(chǔ)字節(jié)表示。int32 的值從 -2147483648 到 2147483647。

預(yù)聲明整數(shù)會(huì)根據(jù)你構(gòu)建代碼時(shí)的體系結(jié)構(gòu)來進(jìn)行映射。在 64 位操作系統(tǒng)上,int 將映射到 int64,而在 32 位系統(tǒng)上,它將映射到 int32。

所有存儲(chǔ)在內(nèi)存中的內(nèi)容都解析為某種數(shù)字類型。在 Go 中,字符串只是一系列 uint8 類型,并包含了一些規(guī)則,用于關(guān)聯(lián)這些字節(jié)和識(shí)別字符串的結(jié)尾位置。

在 Go 中,指針就是 uintptr 類型。同樣地,基于操作系統(tǒng)的體系結(jié)構(gòu),它將映射為 uint32 或者 uint64。Go 為指針創(chuàng)建了一個(gè)特殊的類型。在過去,許多 C 程序員在編寫代碼時(shí),會(huì)認(rèn)為指針值總能符合 unsigned int。隨著時(shí)間的推移,語言和體系結(jié)構(gòu)不斷升級(jí),最終這不再是對的了。由于地址變得比預(yù)先聲明的 unsigned int 更大,很多代碼都出錯(cuò)了。

結(jié)構(gòu)體類型只是很多類型的組合,而這些類型也最終會(huì)解析為數(shù)字類型。

type Example struct{
    BoolValue bool
    IntValue  int16
    FloatValue float32
}

該結(jié)構(gòu)體表示一個(gè)復(fù)雜類型。它表示 7 個(gè)字節(jié),有三種不同的數(shù)字表示。bool 有 1 個(gè)字節(jié),int16 有 2 個(gè)字節(jié),而 float32 有 4 個(gè)字節(jié)。但是,這個(gè)結(jié)構(gòu)體最終在內(nèi)存中分配了 8 個(gè)字節(jié)。

為了最大限度地減少內(nèi)存碎片整理(memory defragmentation),分配內(nèi)存時(shí)都會(huì)將內(nèi)存邊界對齊。要確定 Go 在體系結(jié)構(gòu)上所用的對齊邊界(alignment boundary),你可以運(yùn)行 unsafe.Alignof 函數(shù)。Go 在 64 位 Darwin 平臺(tái)的對齊邊界是 8 個(gè)字節(jié)。因此在 Go 確定我們結(jié)構(gòu)體的內(nèi)存分配時(shí),它將填充字節(jié)以確保最終占用的內(nèi)存是 8 的倍數(shù)。編譯器會(huì)決定在哪里添加填充。
下面的程序會(huì)顯示對于 Example 結(jié)構(gòu)體類型,Go 向內(nèi)存所插入的填充:

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    BoolValue bool
    IntValue int16
    FloatValue float32
}

func main() {
    example := &Example{
        BoolValue:  true,
        IntValue:   10,
        FloatValue: 3.141592,
    }

    exampleNext := &Example{
        BoolValue:  true,
        IntValue:   10,
        FloatValue: 3.141592,
    }

    alignmentBoundary := unsafe.Alignof(example)

    sizeBool := unsafe.Sizeof(example.BoolValue)
    offsetBool := unsafe.Offsetof(example.BoolValue)

    sizeInt := unsafe.Sizeof(example.IntValue)
    offsetInt := unsafe.Offsetof(example.IntValue)

    sizeFloat := unsafe.Sizeof(example.FloatValue)
    offsetFloat := unsafe.Offsetof(example.FloatValue)

    sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
    offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)

    fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)

    fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
        sizeBool, offsetBool, &example.BoolValue)

    fmt.Printf("IntValue = Size: %d Offset: %d Addr: %v\n",
        sizeInt, offsetInt, &example.IntValue)

    fmt.Printf("FloatValue = Size: %d Offset: %d Addr: %v\n",
        sizeFloat, offsetFloat, &example.FloatValue)

    fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
        sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}

輸出如下所示:

Alignment Boundary: 8
BoolValue  = Size: 1  Offset: 0  Addr: 0x21015b018
IntValue   = Size: 2  Offset: 2  Addr: 0x21015b01a
FloatValue = Size: 4  Offset: 4  Addr: 0x21015b01c
Next       = Size: 1  Offset: 0  Addr: 0x21015b020

該結(jié)構(gòu)體類型的對齊邊界的確是 8 字節(jié)。

Size 大小值表示某字段讀寫時(shí)所用的內(nèi)存。不出所料,該值與字段的類型信息相一致。

Offset 偏移值表示字段的開始位置,在內(nèi)存占用中的字節(jié)序號(hào)。

Addr 地址值表示每個(gè)字段開始在內(nèi)存占用中所處的位置。

我們可以看到,Go 在 BoolValue 和 IntValue 字段之間填充了 1 個(gè)字節(jié)。偏移值和兩個(gè)地址之差是 2 個(gè)字節(jié)。你還可以看到,下一個(gè)內(nèi)存分配時(shí)是從結(jié)構(gòu)體最后的字段處分配 4 個(gè)字節(jié)。

我們讓結(jié)構(gòu)體只有一個(gè) bool 字段(1 字節(jié)),來證實(shí) 8 字節(jié)對齊法則。

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    BoolValue bool
}

func main() {
    example := &Example{
        BoolValue:  true,
    }

    exampleNext := &Example{
        BoolValue:  true,
    }

    alignmentBoundary := unsafe.Alignof(example)

    sizeBool := unsafe.Sizeof(example.BoolValue)
    offsetBool := unsafe.Offsetof(example.BoolValue)

    sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
    offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)

    fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)

    fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
        sizeBool, offsetBool, &example.BoolValue)

    fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
        sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}

其輸出如下:

Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0x21015b018
Next      = Size: 1 Offset: 0 Addr: 0x21015b020

把兩個(gè)地址相減,你將看到兩種結(jié)構(gòu)體類型分配之間存在 8 個(gè)字節(jié)的間隙。此外,這一次的內(nèi)存分配從上一示例相同的地址開始。為了保持對齊邊界,Go 向結(jié)構(gòu)體填充了 7 個(gè)字節(jié)。

無論如何填充,Size 值實(shí)際上表示我們可以為每個(gè)字段讀寫的內(nèi)存大小。

我們只能在使用數(shù)字類型時(shí),才能操作內(nèi)存,通過賦值運(yùn)算符(=)可以做到這一點(diǎn)。為了方便,Go 創(chuàng)建了一些可以支持賦值運(yùn)算符的復(fù)雜類型。這些類型有字符串、數(shù)組和切片。要查看這些類型的完整列表,請查看此文檔:http://golang.org/ref/spec#Types。

這些復(fù)雜類型其實(shí)對底層數(shù)字類型進(jìn)行了抽象,我們可以在各種復(fù)雜類型的實(shí)現(xiàn)發(fā)現(xiàn)這一點(diǎn)。在這種情況下,這些復(fù)雜類型可以像數(shù)字類型那樣直接讀取內(nèi)存。

Go 是一種類型安全的語言。這意味著,編譯器將始終強(qiáng)制賦值運(yùn)算符的兩邊類型保持相似。這非常重要,因?yàn)檫@會(huì)防止我們錯(cuò)誤地讀取內(nèi)存。

假設(shè)我們想做下面的事。如果你試圖編譯代碼,你會(huì)得到一個(gè)錯(cuò)誤。

type Example struct{
    BoolValue bool
    IntValue  int16
    FloatValue float32
}

example := &Example{
    BoolValue:  true,
    IntValue:   10,
    FloatValue: 3.141592,
}

var pointer *int32
pointer = *int32(&example.IntValue)
*pointer = 20

我試圖獲取 IntValue 字段(2 個(gè)字節(jié))的內(nèi)存地址,并把它存儲(chǔ)在類型為 int32 的指針上。接下來,我試圖用指針,向內(nèi)存地址寫入一個(gè) 4 個(gè)字節(jié)的整數(shù)。如果可以使用該指針,那么我就會(huì)違反 IntValue 字段的類型規(guī)則,并在此過程中破壞內(nèi)存。

FFE8 FFE7 FFE6 FFE5 FFE4 FFE3 FFE2 FFE1
0 0 0 3.14 0 10 0 true

pointer


FFE3

FFE8 FFE7 FFE6 FFE5 FFE4 FFE3 FFE2 FFE1
0 0 0 0 0 20 0 true

根據(jù)上面的內(nèi)存占用情況,指針將在 FFE3 和 FFE6 之間的 4 個(gè)字節(jié)中寫入 20。IntValue 的值將如預(yù)期的那樣變?yōu)?20,但 FloatValue 的值現(xiàn)在等于 0。想象一下,寫入這些字節(jié)超出了該結(jié)構(gòu)體的內(nèi)存分配,并且開始破壞應(yīng)用的其他區(qū)域的內(nèi)存。隨之而來的錯(cuò)誤會(huì)是隨機(jī)、不可預(yù)測的。

Go 編譯器會(huì)一直保證內(nèi)存對齊和轉(zhuǎn)型是安全的。

在下面一個(gè)轉(zhuǎn)型的示例中,編譯器會(huì)報(bào)錯(cuò):

ackage main

import (
    "fmt"
)

// Create a new type
type int32Ext int32

func main() {
    // Cast the number 10 to a value of type Jill
    var jill int32Ext = 10

    // Assign the value of jill to jack
    // ** cannot use jill (type int32Ext) as type int32 in assignment **
    var jack int32 = jill

    // Assign the value of jill to jack by casting
    // ** the compiler is happy **
    var jack int32 = int32(jill)

    fmt.Printf("%d\n", jack)
}

首先,我們在系統(tǒng)中新建了一個(gè) int32Ext 類型,并告訴編譯器該類型表示一個(gè) int32。接下來,我們創(chuàng)建了一個(gè)名為 jill 的新變量,將其賦值為 10。編譯器允許這個(gè)賦值操作,因?yàn)閿?shù)字類型在賦值運(yùn)算符的右側(cè)。編譯器知道賦值是安全的。

現(xiàn)在,我們嘗試創(chuàng)建第二個(gè)變量,名為 jack,其類型為 int32,我們將 jill 賦值給 jack。在這里,編譯器會(huì)拋出錯(cuò)誤:

cannot use jill (type int32Ext) as type int32 in assignment

編譯器認(rèn)為 jill 的類型是 int32Ext,不會(huì)對賦值的安全性作出任何假設(shè)。

現(xiàn)在我們使用強(qiáng)制轉(zhuǎn)換,編譯器允許賦值,并如預(yù)期打印出值來。當(dāng)我們執(zhí)行轉(zhuǎn)型時(shí),編譯器會(huì)檢查賦值的安全性。在這里,編譯器確定了這是相同類型的值,于是允許賦值操作。

對于某些讀者來說,這似乎很基礎(chǔ),但它是使用任何編程語言的基石。即使類型是經(jīng)過抽象的,你也是在操作內(nèi)存,你應(yīng)該知道你究竟在做些什么。

有了這些基礎(chǔ),我們才可以在 Go 中討論指針,然后將參數(shù)傳遞給函數(shù)。

像往常一樣,我希望這篇文章,能夠幫助你了解一些可能存在的盲區(qū)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容