什麼叫內存屏障、編譯屏障、內存對齊

內存屏障、編譯屏障:

現代 CPU中指令的執行次序不一定按順序執行,沒有相關性的指令可以打亂次序執行,以充分利用 CPU的指令流水線,提高執行速度。同時,編譯器也會對指令進行優化,例如,調整指令順序來利用CPU的指令流水線。這些優化方式,大部分時候都工作良好,但是在一些比較複雜的情況可能會出現錯誤,例如,執行同步代碼時就有可能因爲優化導致同步原語之後的指令在同步原語前執行。

內存屏障和編譯屏障就是用來告訴CPU和編譯器停止優化的手段。編譯屏障是指使用僞指令“memory”告訴編譯器不能把“memory”執行前後的代碼混淆在一起,這時“memory”起到了一種優化屏障的作用。內存屏障是在代碼中使用一些特殊指令,如ARM中的dmb、dsb和isb指令,x86中的sfence、lfence和mfence指令。CPU遇到這些特殊指令後,要等待前面的指令執行完成才執行後面的指令。這些指令的作用就好像一道屏障把前後指令隔離開了,防止CPU把前後兩段指令顛倒執行。

內存對齊:

內存地址對齊,是一種在計算機內存中排列數據(表現爲變量的地址)、訪問數據(表現爲CPU讀取數據)的一種方式,包含了兩種相互獨立又相互關聯的部分:基本數據對齊和結構體數據對齊 。

   爲什麼需要內存對齊?對齊有什麼好處?是我們程序員來手動做內存對齊呢?還是編譯器在進行自動優化的時候完成這項工作?

   在現代計算機體系中,每次讀寫內存中數據,都是按字(word,4個字節,對於X86架構,系統是32位,數據總線和地址總線的寬度都是32位,所以最大的尋址空間爲232 = 4GB(也 許有人會問,我的32位XP用不了4GB內存,關於這個不在本篇博文討論範圍),按A[31,30…2,1,0]這樣排列,但是請注意爲了CPU每次讀寫 4個字節尋址,A[0]和A[1]兩位是不參與尋址計算的。)爲一個塊(chunks)來操作(而對於X64則是8個字節爲一個快)。注意,這裏說的 CPU每次讀取的規則,並不是變量在內存中地址對齊規則。既然是這樣的,如果變量在內存中存儲的時候也按照這樣的對齊規則,就可以加快CPU讀寫內存的速 度,當然也就提高了整個程序的性能,並且性能提升是客觀,雖然當今的CPU的處理數據速度(是指邏輯運算等,不包括取址)遠比內存訪問的速度快,程序的執 行速度的瓶頸往往不是CPU的處理速度不夠,而是內存訪問的延遲,雖然當今CPU中加入了高速緩存用來掩蓋內存訪問的延遲,但是如果高密集的內存訪問,一 種延遲是無可避免的,內存地址對齊會給程序帶來了很大的性能提升。

   內存地址對齊是計算機語言自動進行的,也即是編譯器所做的工作。但這不意味着我們程序員不需要做任何事情,因爲如果我們能夠遵循某些規則,可以讓編譯器做得更好,畢竟編譯器不是萬能的。

   爲了更好理解上面的意思,這裏給出一個示例。在32位系統中,假如一個int變量在內存中的地址是0x00ff42c3,因爲int是佔用4個字節,所以它的尾地址應該是0x00ff42c6,這個時候CPU爲了讀取這個int變量的值,就需要先後讀取兩個word大小的塊,分別是0x00ff42c0~0x00ff42c3和0x00ff42c4~0x00ff42c7,然後通過移位等一系列的操作來得到,在這個計算的過程中還有可能引起一些總線數據錯誤的。但是如果編譯器對變量地址進行了對齊,比如放在0x00ff42c0,CPU就只需要一次就可以讀取到,這樣的話就加快讀取效率。

   1、基本數據對齊
             在X86,32位系統下基於Microsoft、Borland和GNU的編譯器,有如下數據對齊規則:
             a、一個char(佔用1-byte)變量以1-byte對齊。
             b、一個short(佔用2-byte)變量以2-byte對齊。
             c、一個int(佔用4-byte)變量以4-byte對齊。
             d、一個long(佔用4-byte)變量以4-byte對齊。
             e、一個float(佔用4-byte)變量以4-byte對齊。
             f、一個double(佔用8-byte)變量以8-byte對齊。
             g、一個long double(佔用12-byte)變量以4-byte對齊。
             h、任何pointer(佔用4-byte)變量以4-byte對齊。

            而在64位系統下,與上面規則對比有如下不同:
             a、一個long(佔用8-byte)變量以8-byte對齊。
             b、一個double(佔用8-byte)變量以8-byte對齊。
             c、一個long double(佔用16-byte)變量以16-byte對齊。
             d、任何pointer(佔用8-byte)變量以8-byte對齊。

   2、結構體數據對齊
   結構體數據對齊,是指結構體內的各個數據對齊。在結構體中的第一個成員的首地址等於整個結構體的變量的首地址,而後的成員的地址隨着它聲明的順序和實際佔用的字節數遞增。爲了總的結構體大小對齊,會在結構體中插入一些沒有實際意思的字符來填充(padding)結構體。

   在結構體中,成員數據對齊滿足以下規則:
    a、結構體中的第一個成員的首地址也即是結構體變量的首地址。
    b、結構體中的每一個成員的首地址相對於結構體的首地址的偏移量(offset)是該成員數據類型大小的整數倍。
    c、結構體的總大小是對齊模數(對齊模數等於#pragma pack(n)所指定的n與結構體中最大數據類型的成員大小的最小值)的整數倍。

7: struct
8: {
9: char a;
10: int b;
11: short c;
12: char d;
13: }dataAlign;
14:
15: struct
16: {
17: char a;
18: char d;
19: short c;
20: int b;
21:
22: }dataAlign2;

   仔細觀察,會發現雖然是一樣的數據類型的成員,只不過聲明的順序不同,結構體佔用的大小也不同,一個8-byte一個12-byte。爲什麼這樣,下面進行具體分析。  
   首先來看dataAlign2,第一個成員的地址等於結構體變量的首地址,第二個成員char類型,爲了滿足規則b,它相對於結構體的首地址的偏移量必須 是char=1的倍數,由於前面也是char,故不需要在第一個和第一個成員之間填充,直接滿足條件。第三個成員short=2如果要滿足規則b,也不需 要填充,因爲它的偏移量已經是2。同樣第四個也因爲偏移量int=4,不需要填充,這樣結構體總共大小爲8-byte。最後來驗證規則c,在VC中默認 的#pragma pack(n)中的n=8,而結構體中數據類型大小最大的爲第四個成員int=4,故對齊模數爲4,並且8 mode 4 = 0,所以滿足規則c。這樣整個結構體的總大小爲8。

   對於dataAlign,第一個成員等於結構體變量首地址,偏移量爲0,第二個成員爲int=4,爲了滿足規則b,需要在第一個成員之後填充3-byte,讓它相對於結構體首地址偏移量爲4,結合運行結果,可知&dataAlign.a = 0x01109140,而&dataAlign.b = 0x01109144,它們之間相隔4-byte,0x01109141~0x01109143三個字節被0填 充。第三個成員short=2,無需填充滿足規則b。第四個成員char=1,也不需要填充。結構體總大小相加4 + 4 + 2 + 1 = 11。同樣最後需要驗證規則c,結構體中數據類型大小最大爲第二個成員int=4,比VC默認對齊模數8小,故這個結構體的對齊模數仍然爲4,顯然11 mode 4 != 0,故爲了滿足規則c,需要在char後面填充一個字節,這樣結構體變量dataAlign的總大小爲4 + 4 + 2 + 2 = 12。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章