data alignment 和 data padding

開門見山,下面代碼占據(jù)了幾個字節(jié):

struct foo {
    char a;
    int b; 
}

最開始在不懂的時候也以為是5字節(jié),但是如果用sizeof(struct foo)會發(fā)現(xiàn)輸出結果是8。因為在char a后面編譯器為我們填充了一些的內容。使得int是四字節(jié)的對齊的。首先先來明確的描述一下什么是對齊。

alignment

現(xiàn)代的CPU從內存中讀取數(shù)據(jù)的時候,如果數(shù)據(jù)地址和它本身的大小是對齊的(naturally aligned),那么CPU有更好的效率。比如說一個四字節(jié)的數(shù)據(jù),如果他的地址是四字節(jié)對齊的,那么效率更好。至于為什么是這樣,暫時沒有找到一個官方的說法。我不知道為什么會這樣,查了不少資料沒看到確切的說法,但是這里只是描述一下這個現(xiàn)象。
下面是各個數(shù)據(jù)的naturally aligned:摘自 wikipedia

naturally aligned

問題闡述

CPU在訪問內存中的數(shù)據(jù)的時候總是word size的。比如說32bit的數(shù)據(jù)總線。那么每個周期獲得的數(shù)據(jù)就是32 bit。如果數(shù)據(jù)類型小于word size那么,該數(shù)據(jù)總是能在一個周期內讀取到。如果一個數(shù)據(jù)的低地址部分在這個word,另外一部分在高地址部分的word。那么獲得這個數(shù)據(jù)的時候就需要額外的操作來這兩個word的數(shù)據(jù)中不需要的部分去除,然后再拼接為一個所需的部分,導致性能變差。如下圖:


http://www.songho.ca/misc/alignment/dataalign.html

為了避免上述操作,有些CPU會采取一些方法來避免訪問非對齊的數(shù)據(jù)。比如說ARM架構,在ARMv6 ISA之前的CPU強制所有多字節(jié)加載和寫入指令需要對齊。比如說mov dword(舉個例子,我不知道ARM匯編語法)就一定需要對齊。有些CPU會將非對齊的地址通過向下取整到一個對齊的地址,或者有些CPU會直接拋出異常。

數(shù)據(jù)對齊的一些例子:

在C語言中聲明幾個變量:

int c =1;
int *p = &c;
short x =  2;
int main() {
  return 0;
}

在我的電腦上的輸出結果是:

0x404078
0x404080
0x404088

很容易計算出來,int c是四字節(jié)對齊, int *p是八字節(jié)對齊,short x = 2 是2字節(jié)對齊。效果圖如下所示。結果表明符合和前面所說的,和數(shù)據(jù)本身的大小對齊,這樣帶來的性能更好。這也就說明了,在代碼當中,連續(xù)申請的變量在內存中并不是連續(xù)的。

data padding

同樣的問題在結構體當中又會有些稍微的不同。其實,在CPU看來,結構體和單獨的變量并沒有什么不同。編譯器在編譯的時候都會在在兩個變量之間插入一些字節(jié),讓這些變量都是對齊的。還是拿前面的例子

struct foo {
  char a;
  int b;
}

用sizeof來得到這個結構體的大小,結果是8,而不是5。原因是在char a和int b之間有三個字節(jié)的padding。再來看一個例子。

struct foo {
  short a;
  short b;
  short c;
}

這里是否有padding呢?答案是沒有的,一個結構體的地址,就是它第一個變量的地址。所以在分配地址的時候,short a是對齊的,那么很自然的short b也是對齊的。short c與此類似。
再來看一個例子。

struct foo {
  short s;
  char n[3];
}

這個結構體的大小是多少字節(jié)呢? 可能會認為是5。因為此時short在最開始的地方。所以此時short不需要對齊。因為char的長度只有一個字節(jié)。所以它不需要對齊。不過,如果用sizeof來得到這個結構體的大小,輸出是6。在思考這個之前,來看另外一段代碼,假設我們以上這個結構體定義的數(shù)組:

struct foo {
  short s;
  char n[3];
}
struct foo bar[2];

比如說,bar[0]的地址是2(必定是2字節(jié)對齊的,因為要保證short 是2字節(jié)對齊的),那么short s的地址就范圍就是2,2+1這兩個字節(jié),char n[3]的起始地址就是2+2直到2+4。然后bar[1]的地址是2+5開始。實際上這就錯了,為了保證bar[1]中的short也是對齊的。編譯器會在char n[3]后面插入一個字節(jié)的內容。所以sizeof 得到的大小就是6字節(jié),而不是5字節(jié)。
P.S.
對于結構體來說,結構體占據(jù)多少字節(jié)的計算非常簡單,結構體的字節(jié)大小等于它最大的成員變量的整數(shù)倍。比如說結構體中有一個long,那么結構體的大小肯定是8的倍數(shù)。

再來看一個稍微復雜一些的例子。如果在結構體之類又定義了一個結構體,結果又是如何?首先要保證內部的是對齊的,然后還需要確保外部的不會破壞內部的對齊。

struct foo{
   char a;
    struct bar {
      char b;
      int *p;  
  }
}

先來看內部的struct bar這個結構,為了保證int *p對齊,那么char b 之后插入7字節(jié)的空白數(shù)據(jù)為了不破壞內部的對齊結構,從而也要在char a之后插入7字節(jié)的空白數(shù)據(jù)。所以sizeof得到的大小就是24字節(jié)。

如何取消padding

有些時候,我們不需要填充。因為硬件的一些要求,對數(shù)據(jù)結構的字節(jié)數(shù)有要求的,填充就不匹配了。在GNU GCC中,使用attribute((packed))可以取消填充?;蛘咴诮Y構體定義之間加上#pargma pack(1)也行

strcu foo {
  char a;
  long b;
} __attribute__((packed))

#pargma pack(1) 
  strcut foo {
    char a;
    long b;
}

對于這個結構,sizeof()得到的大小就是5字節(jié)。

我們能如何來避免padding嗎

為了對齊插入的padding會增大一個結構體的大小。從而也會導致在內存中空間的浪費,那么我們有什么辦法來避免這個padding呢? 答案也非常簡單,我們在定義的時候,盡量讓大的元素在前面,小的元素在后面。一來就可以避免padding,比如下面的例子。

struct Foo {
    long b;
    short b;
    char c;
}

如果是這樣,就不會存在padding,sizeof得到的大小就是16字節(jié)。如果小的在前,比如說 char c,long a, short b;這樣的順序。此時的大小就變成了24字節(jié)。

一些問題

  1. 如果數(shù)據(jù)類型小于word size,那么是否可以不對齊呢? 因為這樣也可以一個周期就讀取到數(shù)據(jù)。

根據(jù)這篇文章,答案是可以的。雖然說現(xiàn)實中,CPU還是將小于word size的數(shù)據(jù)對齊。至于為什么試想一下,如果word size是32 bit。一個short 可以放在一個word size內的好幾個地方,如下圖藍色,但是不能放在橙色區(qū)域,這樣就到下一個word去了。

image.png

這樣的話其實奇地址看起來也沒問題。但是。難道在設計芯片的時候,我們要去額外判斷,這個類型放置在這個地址是否再+它的大小是否會超過一個word?這明顯沒必要,如果我們只要做到naturally aligend,那么我們一定可以保證它肯定不會被分成兩半。所以, 沒必要。

  1. 如果說CPU每次讀取的數(shù)據(jù)都是word size的,那是否就說明低幾位的地址線沒什么用了?

不是的。以8086為例子,8086的數(shù)據(jù)總線是16bit的。8086總是從偶地址開始讀取。一個word總的低字節(jié)放到數(shù)據(jù)先的0-7位,高字節(jié)放到0-8位。但是8086也支持從內存中讀取單個字節(jié)的數(shù)據(jù)。為了完成這個工作,A0地址線和BHE引腳共同用于決定當前讀取的是單字節(jié)還是一個word。如下圖:

8086

這篇解釋了8086是如何從從不同的地址來讀取不同的尺寸的數(shù)據(jù)。雖然再stackexchange.com里面的回答里面看起來好像A0-A19都是用于8086地址線的。不過根據(jù)8086的一份文檔提到:

Physically, the memory is organized as a high bank (D15–D8) and a low bank (D7–D0) of 512K 8-bit bytes addressed in parallel by the processor’s address lines A19–A1. Byte data with even addresses is transferred on the D7–D0 bus lines while odd addressed byte data (A0 HIGH) is transferred on the D15–D8 bus lines. The processor provides two enable signals, BHE and A0, to selectively allow reading from or writing into either an odd byte location, even byte location, or both. The instruction stream is fetched from memory as words and is addressed internally by the processor to the byte level as necessary

A1-A19是用于地址線的,而A0和BHE是兩個enable signals。所以,并不是說低位的地址線是沒用的。

  1. 那么在現(xiàn)代的CPU情況又是如何呢?
    根據(jù)這篇文章,8086將地址分為odd bank和even bank。odd bank里面放的是奇地址的字節(jié),連接到地址線的D15-D8,even bank里面放的是偶地址的字節(jié),連接到地址線的D7-D0。一個word分別從odd bank和even bank各拿一個字節(jié)。如下圖:

    memory bank

    情況到了32bit的機器,稍微有些不一樣,但是基本的邏輯差不多。在x86的32bit機器下,就變?yōu)槿缦逻@樣的:
    memory bank

    這樣的CPU之下,地址應該要四字節(jié)對齊。所以最低的兩位應該用于byte enable了,就如同前面8086中的A0和BHE一樣。在這里,應該是A0,A1,BHE。我猜想的,沒有找到相關資料。

  2. 現(xiàn)代CPU還需要對齊嗎?
    根據(jù)這篇文檔,現(xiàn)代的CPU可以支持對齊和非對齊的訪問,且沒有性能的差異。但是他提到樹莓派里邊還是影響的。我也在我們的電腦上做了實驗,我的電腦為i5-9400 2.9Ghz。
    代碼如下,分別運行了對齊以及非對齊的情況,發(fā)現(xiàn)兩者時間差不多。代碼如下:

int main() {
    struct Foo {
        char x;
        short a;
        long z;

    } __attribute__((packed));
    struct Foo foo = {1,2,3};
    double used_time;
    clock_t start,end;
    start = clock();
    for (int i = 0; i < 1e7; ++i) {
        foo.z++;
    }
    end = clock();
    used_time = ((double)end-start) / CLOCKS_PER_SEC;
    printf("used time:%lf",used_time);

}
  1. 因為編譯鏈接的地址都是虛擬地址,那么在物理地址的情況下能否保證對齊呢?
    這個疑問來源于,會不會在虛擬地址當中是對齊的。當經(jīng)過頁轉為實際的物理地址以后,能否保證原來的地址還是對齊的。我認為是可以的,原因是一個地址是否對齊,主要看的是這個地址的低位是否有效。比如說八字節(jié)對齊,那么地址只能是以0或者,8結尾的。比如說0x12340,0x12348。無論在哪個頁,低位的地址不會收到頁轉為物理地址的影響。所以我認為還是保證對其的。

References

Accessing odd address memory locations in 8086
The Memory Subsystem
Structure Member Alignment, Padding and Data Packing
The Lost Art of Structure Packing
Data Alignment
intel 8086
Data structure alignment-wikipedia

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容