欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你了解Go中的內(nèi)存對齊

 更新時間:2023年10月31日 10:15:31   作者:燈火消逝的碼頭  
一旦涉及到較為底層的編程,特別是與硬件交互,內(nèi)存對齊是一個必修的課題,所以這篇文章小編就想來和大家聊一聊Go語言中的內(nèi)存對齊,希望對大家有所幫助

前言

在一次工作中,需要使用 Go 調(diào)用 DLL 文件,其中就涉及到內(nèi)存對齊的相關(guān)知識,如果自定義的結(jié)構(gòu)體內(nèi)存布局和所調(diào)用的 DLL 結(jié)構(gòu)體內(nèi)存布局不一致,就會無法正確調(diào)用。所以,一旦涉及到較為底層的編程,特別是與硬件交互,內(nèi)存對齊是一個必修的課題。

基礎(chǔ)知識

在正式了解內(nèi)存對齊前,我們先來看一個方法 unsafe.Sizeof(),它可以獲取任意一個變量占據(jù)的內(nèi)存大小,即這個變量在內(nèi)存中所占據(jù)的字節(jié)數(shù)。Go 內(nèi)置的變量類型占據(jù)內(nèi)存大小情況如下:

類型字節(jié)數(shù)
bool1
string2 * 計算機字長/8 (64位16個字節(jié),32位8個字節(jié))
int、uint、uintptr計算機字長/8 (64位8個字節(jié),32位4個字節(jié))
*T, map, func, chan計算機字長/8 (64位8個字節(jié),32位4個字節(jié))
intN, uintN, floatN, complexNN/8個字節(jié)(int32是4個字節(jié),float64是8個字節(jié))
interface2 * 計算機字長/8 (64位16個字節(jié),32位8個字節(jié))
[]T3 * 計算機字長/8 (64位24個字節(jié),32位12個字節(jié))

對于切片類型而言,字節(jié)數(shù)是固定的24字節(jié)或者12字節(jié)(32位系統(tǒng)上),而對于數(shù)組類型而言,它的大小是元素數(shù)量 * 元素類型字節(jié)數(shù):

var (
	slice []int8
	array [3]int8
)
fmt.Printf("切片:%v\n", unsafe.Sizeof(slice))
fmt.Printf("數(shù)組:%v\n", unsafe.Sizeof(array))

// 結(jié)果
切片:24
數(shù)組:3

對于復(fù)合結(jié)構(gòu),也就是結(jié)構(gòu)體,其情況就會變的復(fù)雜起來:

type MemStruct struct {
	b   bool  // 1
	i8  int8  // 1
	i16 int16 // 2
	i32 int32 // 4
}

type MemStruct2 struct {
	b   bool  // 1
	i8  int8  // 1
	i32 int32 // 4
}

func TestStruct(t *testing.T) {
	fmt.Println("MemStruct:", unsafe.Sizeof(MemStruct{}))
	fmt.Println("MemStruct2:", unsafe.Sizeof(MemStruct2{}))
}

// 結(jié)果
MemStruct: 8
MemStruct2: 8

嗯,看完代碼,是不是發(fā)現(xiàn)了哪里不對,MemStruct 和 MemStruct2 兩個結(jié)構(gòu)體的 SizeOf 值居然是一樣的?MemStruct 還好理解, 1 + 1 + 2 + 4 = 8 當(dāng)然沒問題,那 MemStruct2 是怎么回事?這個問題我們暫且按下不表, 先來看一下 CPU 是怎么訪問內(nèi)存的。

CPU 訪問內(nèi)存

CPU 訪問內(nèi)存時,并不是逐個字節(jié)訪問,而是按照字長位單位訪問,比如 32 位系統(tǒng),CPU 一次性讀取 4 字節(jié),64位則一次性讀取 8 字節(jié)。對于上文的 MemStruct2 結(jié)構(gòu)體,假使它在內(nèi)存中是這樣的(實際上不是,這里是為了方便理解):

那么,此時我們以 4 字長來讀取 Int32 變量,CPU 就必須要讀取兩次內(nèi)存,然后將兩次讀取的結(jié)果進行整理,最終得到完整的數(shù)據(jù):

讀取一個變量需要訪問兩次內(nèi)存訪問?這既不優(yōu)雅也不高效,而且不利于變量操作的原子性。那么,Go 編譯器是怎么解決這個問題呢,可能你也想到了,我們把結(jié)構(gòu)體內(nèi)存調(diào)整一下,在 Int8 之后填充兩個空字節(jié):

經(jīng)過調(diào)整,我們就可以一次性的讀取出 Int32。這種調(diào)整方式有一個響亮的名字——內(nèi)存對齊。內(nèi)存對齊是 Go 編譯器來完成的,它對于程序員是透明的。我們的 MemStruct2 結(jié)構(gòu)體之所以會多出 2 個字節(jié),正是因為內(nèi)存對齊的原因。

為什么需要內(nèi)存對齊

如上文所說,內(nèi)存對齊可以保障變量被 CPU 一次性的讀取出來,這可以減少CPU訪問內(nèi)存的次數(shù),加大CPU訪問內(nèi)存的吞吐量。一次性的讀取變量,也保證變量的原子操作性。除此之外,有些硬件平臺不支持訪問任意地址的任意數(shù)據(jù),如果不進行內(nèi)存對齊,編程語言就喪失了平臺可移植性。內(nèi)存對齊賦予了編程語言的可移植性。

當(dāng)然,內(nèi)存對齊也有一些缺點:

  • 因為會置空一些內(nèi)存,所以會造成一定量的內(nèi)存浪費;
  • 會增加編譯器的復(fù)雜度,編譯器需要根據(jù)不同的平臺和指令集來確定合適的對齊方式,并且需要處理一些特殊的情況,比如位域、聯(lián)合體、指針等。

對齊保證

unsafe.Alignof(x) 返回一個類型的對齊系數(shù),對于 Go 的基礎(chǔ)類型來說,這個值會取 計算機字長/8 和 unsafe.Sizeof(x) 中較小的一個值。即 min(計算機字長/8,unsafe.Sizeof(x))

func TestAlignOf(t *testing.T) {
	var (
		s  string
		i8 int8
	)
	fmt.Printf("string sizeof:%v, alignof: %v\n", unsafe.Sizeof(s), unsafe.Alignof(s)) // min(8, 1) = 1
	fmt.Printf("int8 sizeof:%v, alignof: %v\n", unsafe.Sizeof(i8), unsafe.Alignof(i8)) // min(8, 16) = 16
}

對于 64 位操作系統(tǒng)來說,計算機字長64/8 = 8。int8 的 SizeOf 是 1,與 8 對比較小,所以 int8 的對齊系數(shù)就是1;string 的 SizeOf 是16,與 8 相比較大,所以 string 的對齊系數(shù)就是 8。

對于數(shù)組和結(jié)構(gòu)體類型來說,情況則有些特殊。在 The Go Programming Language Specification 一文中提到了三點,其中后兩點說的就是結(jié)構(gòu)體和數(shù)組:

  • For a variable x of any type: unsafe.Alignof(x) is at least 1.
  • For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
  • For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.

這段話翻譯過來就是:

  • 對于任意類型的變量 x ,unsafe.Alignof(x) 至少為 1;
  • 對于 struct 結(jié)構(gòu)體類型的變量 x,計算 x 每一個字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值;
  • 對于 array 數(shù)組類型的變量 x,unsafe.Alignof(x) 等于構(gòu)成數(shù)組的元素類型的對齊倍數(shù)。

第一點容易理解,沒有哪個類型對齊系數(shù)會小于1的,不然那不就亂套了嗎。第二點的意思就是說對于任意結(jié)構(gòu)體而言,它的對齊系數(shù)會等于它所包含字段中對齊系數(shù)最大的那一個:

type MemStruct struct {
	b   bool   // alignof: 1
	i8  int8   // alignof: 1
	i32 int32  // alignof: 4
	s   string // alignof: 8
}

func TestStruct(t *testing.T) {
	fmt.Printf("MemStruct的對齊系數(shù):{%v}, 等于string的對齊系數(shù):{%v}", unsafe.Alignof(MemStruct{}), unsafe.Alignof(string("1")))
}

// 結(jié)果
MemStruct的對齊系數(shù):{8}, 等于string的對齊系數(shù):{8}

第三點就是說對于數(shù)組類型而言,它的對齊系數(shù)等于它構(gòu)成元素類型的對齊系數(shù):

func TestArray(t *testing.T) {
	var (
		it  interface{}
		arr [3]interface{}
	)
	fmt.Printf("數(shù)組interface{}的對齊系數(shù):{%v}, 等于interface的對齊系數(shù):{%v}", unsafe.Alignof(arr), unsafe.Alignof(it))
}

// 結(jié)果
數(shù)組interface{}的對齊系數(shù):{8}, 等于interface的對齊系數(shù):{8}

以上兩個例子的基于 64 位操作系統(tǒng)。

結(jié)構(gòu)體對齊技巧

合理的布局可以減少內(nèi)存浪費,假使我們現(xiàn)在有一個結(jié)構(gòu)體有 int8、int16、int32 三個字段,那么這三個字段在結(jié)構(gòu)體的順序會影響結(jié)構(gòu)體的內(nèi)存占用嗎?我們來看一個例子:

type S1 struct {
	i8  int8
	i16 int16
	i32 int32
}

type S2 struct {
	i8  int8
	i32 int32
	i16 int16
}

func TestLeastMem(t *testing.T) {
	fmt.Printf("S1的占用: %v\n", unsafe.Sizeof(S1{}))
	fmt.Printf("S2的占用: %v\n", unsafe.Sizeof(S2{}))
}

// 結(jié)果
S1的占用: 8
S2的占用: 12

可以看到,S1 明顯占用的內(nèi)存更少。讓我們來分析一下,S1 和 S2 的對齊系數(shù)都等于其子字段 int16 的對齊系數(shù),也就是 4。對于S1,經(jīng)過內(nèi)存對齊,它們在內(nèi)存中的布局是這樣的:

對于 S1:

  • i8 是第一個字段,默認已經(jīng)對齊,從 0 開始占據(jù) 1 個字節(jié);
  • i16 是第二個字段,對齊系數(shù)為 2,因此,必須填充 1 個字節(jié),其偏移量才是 2 的倍數(shù),從 2 開始占據(jù) 2 字節(jié);
  • i32 是第三個字段,對齊系數(shù)為 4,此時,內(nèi)存已經(jīng)是對齊的,從第 4 開始占據(jù) 4 字節(jié)即可;

因此 S1 在內(nèi)存占用了 8 個字節(jié),浪費了 1 個字節(jié)。

對于 S2:

  • i8 是第一個字段,默認已經(jīng)對齊,從 0 開始占據(jù) 1 個字節(jié);
  • i32 是第二個字段,對齊系數(shù)為 4,因此,必須填充 3 個字節(jié),其偏移量才是 4 的倍數(shù),從第 4 開始占據(jù) 4 字節(jié);
  • i16 是第三個字段,對齊系數(shù)為 2,此時,內(nèi)存已經(jīng)是對齊的,從第 8 開始占據(jù) 2 字節(jié)即可。

因此 S2 在內(nèi)存占用了 12 個字節(jié),浪費了 5 個字節(jié)。

空結(jié)構(gòu)體對齊保證

對于空結(jié)構(gòu)體,其 Sizeof 為0,Alignof 是 1,一般其作為其他結(jié)構(gòu)體字段時,不需要內(nèi)存對齊,但有一種情況除外:在結(jié)構(gòu)體末尾。為什么呢?我們先要知道一件事情:因為空結(jié)構(gòu)體的 Size 為0,所以編譯器會把 zerobase 的地址分配出去,這體現(xiàn)在 src/runtime/malloc.go 878 行中(Go 1.20.4):

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
	...
}

zerobase 是 Go 定義的一個 uintptr 的特殊全局變量,占據(jù) 8 個字節(jié)。因為所有空接口體的地址都指向 zerobase,所以所有空結(jié)構(gòu)體的內(nèi)存地址都是一樣的!這樣做就可以使所有的空結(jié)構(gòu)體有一個獨一無二的內(nèi)存地址,不與 nil 混淆,而且多個空結(jié)構(gòu)體不會占用額外的內(nèi)存。空結(jié)構(gòu)體有內(nèi)存地址卻不占用內(nèi)存,這個概念很重要!

有了這個概念,我們就比較容易理解為什么空結(jié)構(gòu)體在末尾需要內(nèi)存對齊了。當(dāng)空結(jié)構(gòu)體類型作為結(jié)構(gòu)體的最后一個字段時,如果有指向該字段的指針,那么就會返回該結(jié)構(gòu)體之外的地址,導(dǎo)致內(nèi)存泄露。為了避免這種情況就需要進行一次內(nèi)存對齊,且內(nèi)存占用大小和前一個變量的大小保持一致:

type emptyStruct struct{}

type S1 struct {
	empty emptyStruct
	i8    int8
}

type S2 struct {
	i8    int8
	empty emptyStruct
}

type S3 struct {
	i16   int16
	empty emptyStruct
}

type S4 struct {
	i16   int16
	i8    int8
	empty emptyStruct
}

func TestSpaceStructMem(t *testing.T) {
	fmt.Printf("S1的占用: %v\n", unsafe.Sizeof(S1{}))
	fmt.Printf("S2的占用: %v\n", unsafe.Sizeof(S2{}))
	fmt.Printf("S3的占用: %v\n", unsafe.Sizeof(S3{}))
	fmt.Printf("S4的占用: %v\n", unsafe.Sizeof(S4{}))
	// S3 空結(jié)構(gòu)從第二位開始,往后補充兩個字節(jié)
	fmt.Printf("S3的空結(jié)構(gòu)體偏移量: %v\n", unsafe.Offsetof(S3{}.empty))
	// S4 空結(jié)構(gòu)從第三位開始,往后補充一個字節(jié)
	fmt.Printf("S4的空結(jié)構(gòu)體偏移量: %v\n", unsafe.Offsetof(S4{}.empty))
}

// 結(jié)果
S1的占用: 1
S2的占用: 2
S3的占用: 4
S4的占用: 4
S3的空結(jié)構(gòu)體偏移量: 2
S4的空結(jié)構(gòu)體偏移量: 3

以上就是一文帶你了解Go中的內(nèi)存對齊的詳細內(nèi)容,更多關(guān)于go內(nèi)存對齊的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Golang基于Vault實現(xiàn)敏感信息保護

    Golang基于Vault實現(xiàn)敏感信息保護

    Vault?是一個強大的敏感信息管理工具,自帶了多種認證引擎和密碼引擎,本文主要探討應(yīng)用程序如何安全地從?Vault?獲取敏感信息,并進一步實現(xiàn)自動輪轉(zhuǎn),感興趣的可以了解一下
    2023-06-06
  • Go實現(xiàn)MD5加密的三種方法小結(jié)

    Go實現(xiàn)MD5加密的三種方法小結(jié)

    本文主要介紹了Go實現(xiàn)MD5加密的三種方法小結(jié),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • Go中如何使用set的方法示例

    Go中如何使用set的方法示例

    這篇文章主要介紹了Go中如何使用set的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • 基于go interface{}==nil 的幾種坑及原理分析

    基于go interface{}==nil 的幾種坑及原理分析

    這篇文章主要介紹了基于go interface{}==nil 的幾種坑及原理分析,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • 詳解如何用Golang處理每分鐘100萬個請求

    詳解如何用Golang處理每分鐘100萬個請求

    在項目開發(fā)中,我們常常會遇到處理來自數(shù)百萬個端點的大量POST請求,本文主要介紹了Golang實現(xiàn)處理每分鐘100萬個請求的方法,希望對大家有所幫助
    2023-04-04
  • 詳解Golang時間處理的踩坑及解決

    詳解Golang時間處理的踩坑及解決

    在各個語言之中都有時間類型的處理,這篇文章主要和大家分享一下Golang進行時間處理時哪里最容易踩坑以及解決方法,需要的可以參考一下
    2023-01-01
  • 淺析Go項目中的依賴包管理與Go?Module常規(guī)操作

    淺析Go項目中的依賴包管理與Go?Module常規(guī)操作

    這篇文章主要為大家詳細介紹了Go項目中的依賴包管理與Go?Module常規(guī)操作,文中的示例代碼講解詳細,對我們深入了解Go語言有一定的幫助,需要的可以跟隨小編一起學(xué)習(xí)一下
    2023-10-10
  • VSCode1.4 搭建Golang的開發(fā)調(diào)試環(huán)境(遇到很多問題)

    VSCode1.4 搭建Golang的開發(fā)調(diào)試環(huán)境(遇到很多問題)

    這篇文章主要介紹了VSCode1.4 搭建Golang的開發(fā)調(diào)試環(huán)境(遇到很多問題),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-04-04
  • 淺析Go語言中的逃逸分析

    淺析Go語言中的逃逸分析

    在Go語言中,內(nèi)存分配和逃逸分析是至關(guān)重要的概念,對于理解代碼的性能和內(nèi)存使用情況至關(guān)重要,本文將深入探討Go語言中的內(nèi)存分配原理以及逃逸分析的作用,希望對大家有所幫助
    2024-04-04
  • Golang Defer關(guān)鍵字特定操作詳解

    Golang Defer關(guān)鍵字特定操作詳解

    defer是Go語言中的延遲執(zhí)行語句,用來添加函數(shù)結(jié)束時執(zhí)行的代碼,常用于釋放某些已分配的資源、關(guān)閉數(shù)據(jù)庫連接、斷開socket連接、解鎖一個加鎖的資源,這篇文章主要介紹了golang中的defer函數(shù)理解,需要的朋友可以參考下
    2023-03-03

最新評論