<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          詳解內(nèi)存對(duì)齊

          共 6545字,需瀏覽 14分鐘

           ·

          2021-10-20 03:07

          前言

          哈嘍,大家好,我是asong。好久不見,上周停更了一周,因?yàn)楣ぷ饔悬c(diǎn)忙,好在這周末閑了下來(lái),就趕緊來(lái)肝文嘍。今天我們來(lái)聊一聊一道常見的面試八股文——內(nèi)存對(duì)齊,我們平常在業(yè)務(wù)開發(fā)中根本不care內(nèi)存對(duì)齊,但是在面試中,這就是一個(gè)高頻考點(diǎn),今天我們就一起來(lái)看一看到底什么是內(nèi)存對(duì)齊。

          前情概要

          在了解內(nèi)存對(duì)齊之前,先來(lái)明確幾個(gè)關(guān)于操作系統(tǒng)的概念,更加方面我們對(duì)內(nèi)存對(duì)齊的理解。

          • 內(nèi)存管理:我們都知道內(nèi)存是計(jì)算中重要的組成之一,內(nèi)存是與CPU進(jìn)行溝通的橋梁,用于暫存CPU中的運(yùn)算數(shù)據(jù)、以及與硬盤等外部存儲(chǔ)器交換的數(shù)據(jù)。早期,程序是直接運(yùn)行在物理內(nèi)存上的,直接操作物理內(nèi)存,但是會(huì)存在一些問題,比如使用效率低、地址空間不隔離等問題,所以就出現(xiàn)了虛擬內(nèi)存,虛擬內(nèi)存就是在程序和物理內(nèi)存之間引入了一個(gè)中間層,這個(gè)中間層就是虛擬內(nèi)存,這樣就達(dá)到了對(duì)進(jìn)程地址和物理地址的隔離。在linux系統(tǒng)中,將虛擬內(nèi)存劃分為用戶空間內(nèi)核空間,用戶進(jìn)程只能訪問用戶空間的虛擬地址,只有通過(guò)系統(tǒng)調(diào)用、外設(shè)中斷或異常才能訪問內(nèi)核空間,我們主要來(lái)看一下用戶空間,用戶空間被分為5個(gè)不同內(nèi)存區(qū)域:

            內(nèi)存的知識(shí)先介紹個(gè)大概,對(duì)于本文的理解應(yīng)該夠了,我們接著介紹操作系統(tǒng)幾個(gè)其他概念。

            • 代碼段:存放可執(zhí)行文件的操作指令,只讀
            • 數(shù)據(jù)段:用來(lái)存放可執(zhí)行文件中已初始化全局變量,存放靜態(tài)變量和全局變量
            • BSS段:用來(lái)存未初始化的全局變量
            • 棧區(qū):用來(lái)存臨時(shí)創(chuàng)建的局部變量
            • 堆區(qū):用來(lái)存動(dòng)態(tài)分配的內(nèi)存段
          • CPU:中央處理單元(Cntral Pocessing Unit)的縮寫,也叫處理器;CPU是計(jì)算機(jī)的運(yùn)算核心和控制核心,我們?nèi)祟惪恐竽X思考,電腦就是靠著CPU來(lái)運(yùn)算、控制,起到協(xié)調(diào)和控制作用,從功能來(lái)看,CPU 的內(nèi)部由寄存器、控制器、運(yùn)算器和時(shí)鐘四部分組成,各部分之間通過(guò)電信號(hào)連通。

          • CPU和內(nèi)存的工作關(guān)系:當(dāng)我們執(zhí)行一個(gè)程序時(shí),首先由輸入設(shè)備向CPU發(fā)出操作指令,CPU接收到操作指令后,硬盤中對(duì)應(yīng)的程序就會(huì)被直接加載到內(nèi)存中,此后,CPU 再對(duì)內(nèi)存進(jìn)行尋址操作,將加載到內(nèi)存中的指令翻譯出來(lái),而后發(fā)送操作信號(hào)給操作控制器,實(shí)現(xiàn)程序的運(yùn)行或數(shù)據(jù)的處理。存在于內(nèi)存中的目的就是為了CPU能夠過(guò)總線進(jìn)行尋址,取指令、譯碼、執(zhí)行取數(shù)據(jù),內(nèi)存與寄存器交互,然后CPU運(yùn)算,再輸出數(shù)據(jù)至內(nèi)存。

          • osos全稱為Operating System,也就是操作操作系統(tǒng),是一組主管并控制計(jì)算機(jī)操作、運(yùn)用和運(yùn)行硬件、軟件資源和提供公共服務(wù)組織用戶交互的相互關(guān)聯(lián)的系統(tǒng)軟件,同時(shí)也是計(jì)算機(jī)系統(tǒng)的內(nèi)核與基石。
          • 編譯器:編譯器就是將“一種語(yǔ)言(通常為高級(jí)語(yǔ)言)”翻譯為“另一種語(yǔ)言(通常為低級(jí)語(yǔ)言)”的程序。一個(gè)現(xiàn)代編譯器的主要工作流程:源代碼 (source code) → 預(yù)處理器(preprocessor) → 編譯器 (compiler) → 目標(biāo)代碼 (object code) → 鏈接器 (Linker) → 可執(zhí)行程序(executables)。

          寫在最后的一個(gè)知識(shí)點(diǎn):

          計(jì)算機(jī)中,最小的存儲(chǔ)單元為字節(jié),理論上任意地址都可以通過(guò)總線進(jìn)行訪問,每次尋址能傳輸?shù)臄?shù)據(jù)大小就跟CPU位數(shù)有關(guān)。常見的CPU位數(shù)有8位,16位,32位,64位。位數(shù)越高,單次操作執(zhí)行的數(shù)據(jù)量越大,性能也就越強(qiáng)。os的位數(shù)一般與CPU的位數(shù)相匹配,32CPU可以尋址4GB內(nèi)存空間,也可以運(yùn)行32位的os,同樣道理,64位的CPU可以運(yùn)行32位的os,也可以運(yùn)行64位的os

          何為內(nèi)存對(duì)齊

          以下內(nèi)容來(lái)源于網(wǎng)絡(luò)總結(jié):

          現(xiàn)代計(jì)算機(jī)中內(nèi)存空間都是按照字節(jié)(byte)進(jìn)行劃分的,所以從理論上講對(duì)于任何類型的變量訪問都可以從任意地址開始,但是在實(shí)際情況中,在訪問特定類型變量的時(shí)候經(jīng)常在特定的內(nèi)存地址訪問,所以這就需要把各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是按照順序一個(gè)接一個(gè)的排放,這種就稱為內(nèi)存對(duì)齊,內(nèi)存對(duì)齊是指首地址對(duì)齊,而不是說(shuō)每個(gè)變量大小對(duì)齊。

          為何要有內(nèi)存對(duì)齊

          主要原因可以歸結(jié)為兩點(diǎn):

          • 有些CPU可以訪問任意地址上的任意數(shù)據(jù),而有些CPU只能在特定地址訪問數(shù)據(jù),因此不同硬件平臺(tái)具有差異性,這樣的代碼就不具有移植性,如果在編譯時(shí),將分配的內(nèi)存進(jìn)行對(duì)齊,這就具有平臺(tái)可以移植性了
          • CPU每次尋址都是要消費(fèi)時(shí)間的,并且CPU 訪問內(nèi)存時(shí),并不是逐個(gè)字節(jié)訪問,而是以字長(zhǎng)(word size)為單位訪問,所以數(shù)據(jù)結(jié)構(gòu)應(yīng)該盡可能地在自然邊界上對(duì)齊,如果訪問未對(duì)齊的內(nèi)存,處理器需要做兩次內(nèi)存訪問,而對(duì)齊的內(nèi)存訪問僅需要一次訪問,內(nèi)存對(duì)齊后可以提升性能。舉個(gè)例子:

          假設(shè)當(dāng)前CPU32位的,并且沒有內(nèi)存對(duì)齊機(jī)制,數(shù)據(jù)可以任意存放,現(xiàn)在有一個(gè)int32變量占4byte,存放地址在0x00000002 - 0x00000005(純假設(shè)地址,莫當(dāng)真),這種情況下,每次取4字節(jié)的CPU第一次取到[0x00000000 - 0x00000003],只得到變量1/2的數(shù)據(jù),所以還需要取第二次,為了得到一個(gè)int32類型的變量,需要訪問兩次內(nèi)存并做拼接處理,影響性能。如果有內(nèi)存對(duì)齊了,int32類型數(shù)據(jù)就會(huì)按照對(duì)齊規(guī)則在內(nèi)存中,上面這個(gè)例子就會(huì)存在地址0x00000000處開始,那么處理器在取數(shù)據(jù)時(shí)一次性就能將數(shù)據(jù)讀出來(lái)了,而且不需要做額外的操作,使用空間換時(shí)間,提高了效率。

          沒有內(nèi)存對(duì)齊機(jī)制:

          內(nèi)存對(duì)齊后:

          對(duì)齊系數(shù)

          每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)"對(duì)齊系數(shù)",常用平臺(tái)默認(rèn)對(duì)齊系數(shù)如下:

          • 32位系統(tǒng)對(duì)齊系數(shù)是4
          • 64位系統(tǒng)對(duì)齊系數(shù)是8

          這只是默認(rèn)對(duì)齊系數(shù),實(shí)際上對(duì)齊系數(shù)我們是可以修改的,之前寫C語(yǔ)言的朋友知道,可以通過(guò)預(yù)編譯指令#pragma pack(n)來(lái)修改對(duì)齊系數(shù),因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">C語(yǔ)言是預(yù)處理器的,但是在Go語(yǔ)言中沒有預(yù)處理器,只能通過(guò)tags命名約定來(lái)讓Go的包可以管理不同平臺(tái)的代碼,但是怎么修改對(duì)齊系數(shù),感覺Go并沒有開放這個(gè)參數(shù),找了好久沒有找到,等后面再仔細(xì)看看,找到了再來(lái)更新!

          既然對(duì)齊系數(shù)無(wú)法更改,但是我們可以查看對(duì)齊系數(shù),使用Go語(yǔ)言中的unsafe.Alignof可以返回相應(yīng)類型的對(duì)齊系數(shù),使用我的mac(64位)測(cè)試后發(fā)現(xiàn),對(duì)齊系數(shù)都符合2^n這個(gè)規(guī)律,最大也不會(huì)超過(guò)8

          func?main()??{
          ?fmt.Printf("string?alignof?is?%d\n",?unsafe.Alignof(string("a")))
          ?fmt.Printf("complex128?alignof?is?%d\n",?unsafe.Alignof(complex128(0)))
          ?fmt.Printf("int?alignof?is?%d\n",?unsafe.Alignof(int(0)))
          }
          運(yùn)行結(jié)果
          string?alignof?is?8
          complex128?alignof?is?8
          int?alignof?is?8

          注意:不同硬件平臺(tái)占用的大小和對(duì)齊值都可能是不一樣的。

          結(jié)構(gòu)體的內(nèi)存對(duì)齊規(guī)則

          一提到內(nèi)存對(duì)齊,大家都喜歡拿結(jié)構(gòu)體的內(nèi)存對(duì)齊來(lái)舉例子,這里要提醒大家一下,不要混淆了一個(gè)概念,其他類型也都是要內(nèi)存對(duì)齊的,只不過(guò)拿結(jié)構(gòu)體來(lái)舉例子能更好的理解內(nèi)存對(duì)齊,并且結(jié)構(gòu)體中的成員變量對(duì)齊有自己的規(guī)則,我們需要搞清這個(gè)對(duì)齊規(guī)則。

          C語(yǔ)言的對(duì)齊規(guī)則與Go語(yǔ)言一樣,所以C語(yǔ)言的對(duì)齊規(guī)則對(duì)Go同樣適用:

          • 對(duì)于結(jié)構(gòu)體的各個(gè)成員,第一個(gè)成員位于偏移為0的位置,結(jié)構(gòu)體第一個(gè)成員的偏移量(offset)為0,以后每個(gè)成員相對(duì)于結(jié)構(gòu)體首地址的offset都是該成員大小與有效對(duì)齊值中較小那個(gè)的整數(shù)倍,如有需要編譯器會(huì)在成員之間加上填充字節(jié)。
          • 除了結(jié)構(gòu)成員需要對(duì)齊,結(jié)構(gòu)本身也需要對(duì)齊,結(jié)構(gòu)的長(zhǎng)度必須是編譯器默認(rèn)的對(duì)齊長(zhǎng)度和成員中最長(zhǎng)類型中最小的數(shù)據(jù)大小的倍數(shù)對(duì)齊。

          舉個(gè)例子

          根據(jù)上面的對(duì)齊規(guī)則,我們來(lái)分析一個(gè)例子,加深理解:

          //?64位平臺(tái),對(duì)齊參數(shù)是8
          type?User?struct?{
          ?A?int32?//?4
          ?B?[]int32?//?24
          ?C?string?//?16
          ?D?bool?//?1
          }

          func?main()??{
          ?var?u?User
          ?fmt.Println("u1?size?is?",unsafe.Sizeof(u))
          }
          //?運(yùn)行結(jié)果
          u?size?is??56

          這里我的mac64位的,對(duì)齊參數(shù)是8int32[]int32stringbool對(duì)齊值分別是4881,占用內(nèi)存大小分別是424161,我們先根據(jù)第一條對(duì)齊規(guī)則分析User

          • 第一個(gè)字段類型是int32,對(duì)齊值是4,大小為4,所以放在內(nèi)存布局中的第一位.
          • 第二個(gè)字段類型是[]int32,對(duì)齊值是8,大小為24,按照第一條規(guī)則,偏移量應(yīng)該是成員大小24與對(duì)齊值8中較小那個(gè)的整數(shù)倍,那么偏移量就是8,所以4-7位會(huì)由編譯進(jìn)行填充,一般為0值,也稱為空洞,第932位為第二個(gè)字段B.
          • 第三個(gè)字段類型是string,對(duì)齊值是8,大小為16,所以他的內(nèi)存偏移值必須是8的倍數(shù),因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">user前兩個(gè)字段就已經(jīng)排到了第32位,所以offset32正好是8的倍數(shù),不要填充,從32位到48位是第三個(gè)字段C.
          • 第四個(gè)字段類型是bool,對(duì)齊值是1,大小為1,所以他的內(nèi)存偏移值必須是1的倍數(shù),因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">user前兩個(gè)字段就已經(jīng)排到了第48位,所以下一位的偏移量正好是48,正好是字段D的對(duì)齊值的倍數(shù),不用填充,可以直接排列到第四個(gè)字段,也就是從48到第49位是第三個(gè)字段D.

          根據(jù)第一條規(guī)則分析后,現(xiàn)在結(jié)構(gòu)所占大小為49字節(jié),我們?cè)賮?lái)根據(jù)第二條規(guī)則分析:

          • 根據(jù)第二條規(guī)則,默認(rèn)對(duì)齊值是8,字段中最大類型程度是24,所以求出結(jié)構(gòu)體的對(duì)齊值是8,我們目前的內(nèi)存長(zhǎng)度是49,不是8的倍數(shù),所以需要補(bǔ)齊,所以最終的結(jié)果就是56,補(bǔ)了7位。

          成員變量順序?qū)?nèi)存對(duì)齊帶來(lái)的影響

          根據(jù)上面的規(guī)則我們可以看出,成員變量的順序也會(huì)影響內(nèi)存對(duì)齊的結(jié)果,我們先來(lái)看一個(gè)例子:

          type?test1?struct?{
          ?a?bool?//?1
          ?b?int32?//?4
          ?c?string?//?16
          }

          type?test2?struct?{
          ?a?int32?//?4
          ?b?string?//?16
          ?c?bool?//?1
          }


          func?main()??{
          ?var?t1?test1
          ?var?t2?test2

          ?fmt.Println("t1?size?is?",unsafe.Sizeof(t1))
          ?fmt.Println("t2?size?is?",unsafe.Sizeof(t2))
          }

          運(yùn)行結(jié)果:

          t1?size?is??24
          t2?size?is??32

          test1的內(nèi)存布局:

          test2的內(nèi)存布局:

          )

          通過(guò)以上分析,我們可以看出,結(jié)構(gòu)體中成員變量的順序會(huì)影響結(jié)構(gòu)體的內(nèi)存布局,所以在日常開發(fā)中大家要注意這個(gè)問題,可以節(jié)省內(nèi)存空間。

          空結(jié)構(gòu)體字段對(duì)齊

          Go語(yǔ)言中空結(jié)構(gòu)體的大小為0,如果一個(gè)結(jié)構(gòu)體中包含空結(jié)構(gòu)體類型的字段時(shí),通常是不需要進(jìn)行內(nèi)存對(duì)齊的,舉個(gè)例子:

          type?demo1?struct?{
          ?a?struct{}
          ?b?int32
          }

          func?main()??{
          ?fmt.Println(unsafe.Sizeof(demo1{}))
          }
          運(yùn)行結(jié)果:
          4

          從運(yùn)行結(jié)果可知結(jié)構(gòu)體demo1占用的內(nèi)存與字段b占用內(nèi)存大小相同,所以字段a是沒有占用內(nèi)存的,但是空結(jié)構(gòu)體有一個(gè)特例,那就是當(dāng) struct{} 作為結(jié)構(gòu)體最后一個(gè)字段時(shí),需要內(nèi)存對(duì)齊。因?yàn)槿绻兄羔樦赶蛟撟侄? 返回的地址將在結(jié)構(gòu)體之外,如果此指針一直存活不釋放對(duì)應(yīng)的內(nèi)存,就會(huì)有內(nèi)存泄露的問題(該內(nèi)存不因結(jié)構(gòu)體釋放而釋放),所以當(dāng)struct{}作為結(jié)構(gòu)體成員中最后一個(gè)字段時(shí),要填充額外的內(nèi)存保證安全。

          type?demo2?struct?{
          ?a?int32
          ?b?struct{}
          }

          func?main()??{
          ?fmt.Println(unsafe.Sizeof(demo2{}))
          }
          運(yùn)行結(jié)果:
          8

          考慮內(nèi)存對(duì)齊的設(shè)計(jì)

          在之前的文章源碼剖析sync.WaitGroup分析sync.waitgroup的源碼時(shí),使用state1來(lái)存儲(chǔ)狀態(tài):

          //?A?WaitGroup?must?not?be?copied?after?first?use.
          type?WaitGroup?struct?{
          ?noCopy?noCopy

          ?//?64-bit?value:?high?32?bits?are?counter,?low?32?bits?are?waiter?count.
          ?//?64-bit?atomic?operations?require?64-bit?alignment,?but?32-bit
          ?//?compilers?do?not?ensure?it.?So?we?allocate?12?bytes?and?then?use
          ?//?the?aligned?8?bytes?in?them?as?state,?and?the?other?4?as?storage
          ?//?for?the?sema.
          ?state1?[3]uint32
          }

          state1這里總共被分配了12個(gè)字節(jié),這里被設(shè)計(jì)了三種狀態(tài):

          • 其中對(duì)齊的8個(gè)字節(jié)作為狀態(tài),高32位為計(jì)數(shù)的數(shù)量,低32位為等待的goroutine數(shù)量
          • 其中的4個(gè)字節(jié)作為信號(hào)量存儲(chǔ)

          提供了(wg *WaitGroup) state() (statep *uint64, semap *uint32)幫助我們從state1字段中取出他的狀態(tài)和信號(hào)量,為什么要這樣設(shè)計(jì)呢?

          因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">64位原子操作需要64位對(duì)齊,但是32位編譯器不能保證這一點(diǎn),所以為了保證waitGroup32位平臺(tái)上使用的話,就必須保證在任何時(shí)候,64位操作不會(huì)報(bào)錯(cuò)。所以也就不能分成兩個(gè)字段來(lái)寫,考慮到字段順序不同、平臺(tái)不同,內(nèi)存對(duì)齊也就不同。因此這里采用動(dòng)態(tài)識(shí)別當(dāng)前我們操作的64位數(shù)到底是不是在8字節(jié)對(duì)齊的位置上面,我們來(lái)分析一下state方法:

          //?state?returns?pointers?to?the?state?and?sema?fields?stored?within?wg.state1.
          func?(wg?*WaitGroup)?state()?(statep?*uint64,?semap?*uint32)?{
          ?if?uintptr(unsafe.Pointer(&wg.state1))%8?==?0?{
          ??return?(*uint64)(unsafe.Pointer(&wg.state1)),?&wg.state1[2]
          ?}?else?{
          ??return?(*uint64)(unsafe.Pointer(&wg.state1[1])),?&wg.state1[0]
          ?}
          }

          當(dāng)數(shù)組的首地址是處于一個(gè)8字節(jié)對(duì)齊的位置上時(shí),那么就將這個(gè)數(shù)組的前8個(gè)字節(jié)作為64位值使用表示狀態(tài),后4個(gè)字節(jié)作為32位值表示信號(hào)量(semaphore)。同理如果首地址沒有處于8字節(jié)對(duì)齊的位置上時(shí),那么就將前4個(gè)字節(jié)作為semaphore,后8個(gè)字節(jié)作為64位數(shù)值。畫個(gè)圖表示一下:

          )

          總結(jié)

          終于接近尾聲了,內(nèi)存對(duì)齊一直面試中的高頻考點(diǎn),通過(guò)內(nèi)存對(duì)齊可以了解面試者對(duì)操作系統(tǒng)知識(shí)的了解程度,所以這塊知識(shí)還是比較重要的,希望這篇文章能幫助大家答疑解惑,更好的忽悠面試官~。

          文中代碼已上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/memory 歡迎star;

          文中有任何問題歡迎留言區(qū)探討~;

          素質(zhì)三連(分享、點(diǎn)贊、在看)都是筆者持續(xù)創(chuàng)作更多優(yōu)質(zhì)內(nèi)容的動(dòng)力!我是asong,我們下期見。

          瀏覽 105
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  先锋影音亚洲AV每日资源网站 | 亚洲性爱城| 天天看黄 | 国产资源网 | 美女操屄视频 |