Go 的記憶體對齊和指標運算詳解和實踐

a_wei發表於2020-01-05

uintptr 和 unsafe普及

uintptr

在Go的原始碼中uintptr的定義如下:

/* uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
從英文註釋可以看出 uintptr是一個整形,它的大小能夠容納任何指標的位模式,它是無符號的,最大值為:18446744073709551615,怎麼來的,int64最大值 * 2 +1  
*/
type uintptr uintptr

位模式:記憶體由位元組組成.每個位元組由8位bit組成,每個bit狀態只能是0或1.所謂位模式,就是變數所佔用記憶體的所有bit的狀態的序列
指標大小:一個指標的大小是多少呢?在32位作業系統上,指標大小是4個位元組,在64位作業系統上,指標的大小是8位元組,
所以uintptr能夠容納任何指標的位模式,總的說uintptr表示的指標地址的值,可以用來進行數值計算
GC不會把uintptr當作指標,uintptr不會持有一個物件,uintptr型別的目標會被GC回收

unasfe

在Go中,unsafe是一個包,內容也比較簡短,但註釋非常多,這個包主要是用來在一些底層程式設計中,讓你能夠操作記憶體地址計算,也就是說Go本身是不支援指標運算,但還是留了一個後門,而且Go也不建議研發人員直接使用unsafe包的方法,因為它繞過了Go的記憶體安全原則,是不安全的,容易使你的程式出現莫名其妙的問題,不利於程式的擴充套件與維護但為什麼說它呢,因為很多框架包括SDK中的原始碼都用到了這個包的知識,在看原始碼時這塊不懂,容易懵。下面看看這個包定義了什麼?

//ArbitraryType的型別也是int,但它被賦予特殊的含義,代表一個Go的任意表示式型別
type ArbitraryType int

//Pointer是一個int指標型別,在Go種,它是所有指標型別的父型別,也就是說所有的指標型別都可以轉化為Pointer, uintptr和Pointer可以相互轉化
type Pointer *ArbitraryType

//返回指標變數在記憶體中佔用的位元組數(記住,不是變數對應的值佔用的位元組數)
func Sizeof(x ArbitraryType) uintptr

/*Offsetof返回變數指定屬性的偏移量,這個函式雖然接收的是任何型別的變數,但是有一個前提,就是變數要是一個struct型別,且還不能直接將這個struct型別的變數當作引數,只能將這個struct型別變數的屬性當作引數*/
func Offsetof(x ArbitraryType) uintptr

//返回變數對齊位元組數量
func Alignof(x ArbitraryType) uintptr

什麼是記憶體對齊?為什麼要記憶體對齊?

在我瞭解比較深入的語言中(Java Go)都有記憶體對齊的概念,百度百科對記憶體對齊的概念是這樣定義的:“記憶體對齊”應該是編譯器的“管轄範圍”。編譯器為程式中的每個“資料單元”安排在適當的位置上,所謂的資料單元其實就是變數的值。

為什麼要記憶體對齊呢?

  1. 平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常(32位平臺上執行64位平臺上編譯的程式要求必須8位元組對齊,否則發生panic)
  2. 效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問

對齊規則:也就是對齊的邊界,多少個位元組記憶體對齊,在32位作業系統上,是4個自己,在64位作業系統上是8個位元組

透過一幅圖來理解上面的內容,下圖只是舉個例子,位數並沒有畫全

Go

指標運算和記憶體對齊實踐

記憶體對齊實踐

理論總是枯燥的,但必須瞭解,也許看了理論還是不懂,接下來透過實踐讓你明白

//建立一個變數
var i int8 = 10

//建一個變數轉化成Pointer 和 uintptr
p := unsafe.Pointer(&i) //入參必須是指標型別的
fmt.Println(p) //是記憶體地址0xc0000182da
u := uintptr(i)
fmt.Println(u) //結果就是10

//Pointer轉換成uintptr
temp := uintptr(p)
//uintptr轉Pointer
p= unsafe.Pointer(u)

//獲取指標大小
u = unsafe.Sizeof(p) //傳入指標,獲取的是指標的大小
fmt.Println(u) // 列印u是:8
 //獲取的是變數的大小
u = unsafe.Sizeof(i)
fmt.Println(u) //列印u是:1

//建立兩個個結構體
type Person1 struct{
    a bool
    b int64
    c int8
    d string
}
type Person2 struct{
    b int64
    c int8
    a bool
    d string
}
//接下來演示一下記憶體對齊,猜一猜下面l兩個列印值是多少呢?
person1 := Person1{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Sizeof(person1))
person2 := Person2{b:1,c:1,a:true,d:"spw"}
fmt.Println(unsafe.Sizeof(person2))
//第一個結果是40,第二個結果是32,為什麼會有這些差距呢?其實就是記憶體對齊做的鬼,我來詳細解釋一下

我們知道在Person1和Person2種變數型別都一樣,只是順序不太一樣,
bool佔1個位元組,
int64佔8個位元組,
int8佔一個位元組,
string佔用16個位元組,
總的結果應該是 1+8+1+16= 26,為啥Person1是40呢,Person2是32,看下圖

Go

根據上圖,我們就明白了,在結構體編寫中存在記憶體對齊的概念,而且我們應該小心,儘可能的避免因記憶體對齊導致結構體大小增大,在書寫過程中應該讓小位元組的變數挨著。我們可以工具進行檢測(golangci-lint)。

我們可以透過func Alignof(x ArbitraryType) uintptr這個方法返回記憶體對齊的位元組數量,如下程式碼

type Person1 struct{
    a bool
    b int64
    c int8
    d string
}
p := Person{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Alignof(person))
type Person2 struct{
    a bool
    c int8
}
p1 := Person1{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Alignof(p1))
p2 := Person2{a:true,c:1}
fmt.Println(unsafe.Alignof(p2))
//你任務上面兩個println列印多少呢?結果是8,1,在結構體中,記憶體對齊是按照結構體中最大位元組數對齊的(但不會超過8)
指標運算實踐

我們還是用程式碼來舉例說明


type W struct {
   b int32
   c int64
}
var w *W = new(W)
//這時w的變數列印出來都是預設值0,0
fmt.Println(w.b,w.c)

//現在我們透過指標運算給b變數賦值為10
b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))
*((*int)(b)) = 10
//此時結果就變成了10,0
fmt.Println(w.b,w.c)

解釋一下上面的程式碼
uintptr(unsafe.Pointer(w))獲取了w的指標起始值,
unsafe.Offsetof(w.b) 獲取b變數的偏移量
兩個相加就得到了b的地址值,將通用指標Pointer轉換成具體指標((*int)(b)),透過 符號取值,然後賦值,((int)(b)) 相當於把(int) 轉換成 int了,最後對變數重新賦值成10,這樣指標運算就完成了。

關注微信公眾號,閱讀更多精彩文章

Go

本作品採用《CC 協議》,轉載必須註明作者和本文連結
那小子阿偉

相關文章