Go的內存對齊和指針運算詳解和實踐

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個字節

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

指針運算和內存對齊實踐

內存對齊實踐

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

//創建一個變量
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,看下圖

根據上圖,我們就明白了,在結構體編寫中存在內存對齊的概念,而且我們應該小心,儘可能的避免因內存對齊導致結構體大小增大,在書寫過程中應該讓小字節的變量挨着。我們可以工具進行檢測(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,這樣指針運算就完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章