1. 程式人生 > >c++內存對齊 轉載

c++內存對齊 轉載

pragma mage 證明 clas 內存布局 存儲 不同的 ret float

轉載自http://blog.csdn.net/chengonghao/article/details/51674166

例子舉的特別好

很多文章大概都有像這樣的結論:

1. 數據項只能存儲在地址是數據項大小的整數倍的內存位置上;

2. 結構體變量的首地址能夠被其最寬基本類型成員的大小所整除;

3. 對齊在N上,也就是說該數據的"存放起始地址%N=0

很明顯,如果對數據存放地址的把握錯誤了的話,那麽由此推斷出來的地址對齊規則也就全都是錯的了,而事實上也是如此。我研究這個課題 80%的時間都是花在這個上面,而真正的對齊規則一個下午應該就可以解決了。

當然,像上面的結論在一般情況下基本上是正確的,也就是說:

char變量的地址 %1 =0;

short變量地址 %2 =0;

int變量的地址 %4 =0;

double變量的地址 %8 =0

這裏假設:

sizeof(char) = 1;

sizeof(short) = 2;

sizeof(int)=4;

sizeof(double)=8

這是 win32 平臺上的實際值,此篇都以此假設為基礎。

當這些變量是處於內存的數據區(或只讀數據區)或者是從堆上分配出來的話,應該都是正確的,因為編譯器和堆管理可能會幫你把這件事情做得很好,而程序員在代碼裏面基本上控制不了這些區域的變量的起始地址,實則我的多次實際測試也都符合上面的結論,即變量存放起始地址%N=0,結構體也符合首地址能夠被其最寬基本類型成員的大小所整除。

而唯一我們比較好靈活控制的就是棧上數據,也就是局部變量。在 32 位的系統上棧的單位大小是 32 bit,即 4 字節,每一個棧的地址肯定也是 %4 = 0 的,如果一個棧存放了 char / short / int 的話那麽他們肯定也滿足 % N = 0。我們唯一可以找到破綻的就是使用一個8字節的數據,也就是 double,通過對棧上數據的巧妙安排讓 double 變量的地址處於一個可以讓 4 整除而不可以讓 8 整除的地址上,那麽我們的目的就達到了,"存放起始地址 % N = 0 "的結論即可推翻。當然,熟悉棧布局的話這些是可以輕易做到的。具體可以按如下步驟實驗。

技術分享
#include "stdafx.h"  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
  
    double a = 0;  
  
    double b = 0;  
  
    double c = 0;  
  
    printf("&a=0x%X, &b=0x%X, &c=0x%X", &a, &b, &c);  
    getchar();  
    return 0;  
}  
技術分享

以下是 VS2013 上的輸出

技術分享

可以看到,變量地址的最後一位是 4,不能被 8 整除(double 為 8 字節),因此已經可以推翻結論1了。

得到的這些地址可能有些隨機性,這些地址依賴於具體的環境和編譯參數等,但是,無論如何,我們實際看到的東西已經可以推翻上面結論 1 對地址假設的錯誤的結論了。我們再來看結構體的情況,把他們徹底推翻!為了讓內部類型的變量一定可以按照其自身的對齊參數對齊,我們指定對齊參數設為16。

技術分享
#include "stdafx.h"  
  
#pragma pack(16)  
  
struct A{  
  
    char a;  
  
    double b;  
  
};  
  
  
  
int _tmain(int argc, _TCHAR* argv[]){  
  
    struct A a;  
  
    struct A b;  
  
    struct A c;  
  
    printf("&a=0x%X, &b=0x%X, &c=0x%X\n", &a, &b, &c);  
  
    printf("&a.b=0x%X, &b.b=0x%X, &c.b=0x%X", &a.b, &b.b, &c.b);  
  
    getchar();  
    return 1;  
  
}  
技術分享

技術分享

由上面可以看到,結構體 A 的最大長度成員的類型是 double,就是成員變量 b 。 可以看到,結構體變量的起始地址不能被8整除,結構體中的 double 成員的地址也不能被 8 整除,當然這個的原因還是結構體變量的起始地址不能被8整除導致 double 的成員也不能被 8 整除。

我們實際看到的東西已經可以推翻上面結論 2 對地址假設的錯誤的結論了

盡管在不同的編譯環境和系統上會有不同的值,但是從上面的兩個實驗我們確實得到了不滿足那些結論的值,也就是說,無論如何,那些對變量起始地址的假設和結論一定錯了~!

我本來以為我對棧布局還比較熟悉,通過多次試驗是不是可以完全預測,但是後來證明這些一切都是徒勞,在不同的編譯環境下,特別是編譯器的優化選項,棧的存放簡直就是五花八門,根本預測不到,我們確實不能對變量的地址做過多的假設。

2. 地址的字節對齊規則

地址對齊的規則也不是很復雜,只要把對起始地址有假設的那些結論稍微改一下基本上就差不多了。以下我先給出我自己總結的一個版本,然後再慢慢論證與解析。

編譯器都有一個指定的對齊參數用於 structure, union, and class 成員,在 win32 平臺上的編譯器都是默認為 8,這個指定的對齊參數可以在代碼裏面使用 pack(n) 指令指定,n合法的值是1,2,4,8,16。

每個內部類型自身也都有一個自己的對齊參數,一般來說這個對齊參數就是 sizeof(具體type) 的值,在 win32 平臺上就是采用sizeof作為具體類型的自身對齊參數的,也就是講,char 的自身對齊參數是 1 , short 是 2 , int 是 4, float 也是 4, double 是 8 等。

地址對齊是相對於結構的成員來說的,單個內部類型的變量這種就沒什麽對齊不對齊的說法了。

結構的成員按照結構中聲明的順序依次排放,對齊的意思是成員相對於結構變量的起始地址的相對對齊,關鍵是在於相對於結構變量的起始地址的偏移。

有效對齊參數,內部類型的有效對齊是指它的自身對齊參數和指定對齊參數中較小的那個對齊參數;結構類型的有效對齊參數是指它的成員中,有效對齊參數最大的那個值。數組的有效對齊就是它的成員類型的有效對齊。

有了這些就可以得出對齊規則了:

1. (成員的起始地址相對於結構的起始地址的偏移) % (成員的有效對齊) == 0

2. (結構的總大小) % (結構的有效對齊) == 0

3. 如果無法滿足對齊規則的話就填充字節直到滿足對齊規則

從上面可以看到,如果指定的對齊參數大於了變量的自身對齊參數的話,指定的對齊參數將不起作用,這就是之前為什麽要 #pragma pack(16) 的原因了,使得指定對齊參數沒用,各個變量按照自己的類型的自身對齊參數對齊。

結構的總大小也要求符合對齊規則,主要是考慮到了結構體數組的情況,數組的各個成員是緊密排列的,不會有空隙,如果結構總大小滿足對齊要求的話那麽整個數組就自然滿足對齊要求了,如果總大小不滿足對齊要求的話,數組各個成員又要緊密排列,那麽這個對齊就又沒意義了,CPU 讀取這些數組成員還是要花多余的開銷。

說了這麽多,還是舉例子講話來得實在。

# pragma pack(16)  
struct A{  
      char a;  
      double b;  
};  

第一個成員的地址就是結構的起始地址,所以它的地址相對於結構的起始地址的偏移是 0,而 a 是 char 類型,它的自身對齊是 1 小於指定的對齊參數 16,所以 a 的有效對齊是1,a 的起始地址偏移也滿足 0 % 1 = 0;第二個成員是 double 類型,其自身對齊參數是 8,也小於指定的對齊參數,所以它的有效對齊是 8,這樣我們指定的 # pragma pack(16) 就相當於一點用都沒有了。而 double 類型的成員 b 要想滿足對齊規則就必須在 a 的後面填充字節以使得 b 的地址相對於結構的起始地址的偏移至少為 8。所以結構 A的內存布局會是這樣:

00 CC CC CC CC CCCC CC 00 00 00 00 00 00 00 00    

而 sizeof(A) = 16;0 分別表示 a 和 b 的位置,CC就是填充的 7 個字節。

  1. # pragma pack(16)
  2. struct A{
  3. char a;
  4. short c;
  5. double b;
  6. };



同樣指定的對齊參數仍然沒任何作用,a 還是在偏移為 0 的地址上,c 在 a 之後,c 的有效對齊就是自身對齊 2,位於相對起始地址偏移為 2 的地址上,滿足對齊要求 2 % 2 = 0,c 和 a 之間填充了 1 個字節。b 仍然位於偏移地址為 8 的地址上,b 和 c 之間填充了 4 個字節。結構 A 的內存布局如下:

# pragma pack(16)  
struct A{  
      char a;  
      short c;  
      double b;  
};  

  1. 00 CC 00 00 CC CCCC CC 00 00 00 00 00 00 00 00



而 sizeof(A) = 16;0 分別代表 a,c,b的位置,CC 就是填充字節。

# pragma pack(16)  
struct A{  
      char a;  
      double b;  
      short c;  
};  

指定對齊仍然沒用,a 在偏移為 0 的地址上,b 在偏移為 8 的地址上,c 緊緊挨著 b 的屁股,因為此時的地址偏移 16 已經滿足 c 的對齊要求 16 % 2 = 0;所以就沒必要填充字節了。但是結構體 A 的總大小也要滿足對齊規則的第二條,即 (結構的總大小)%(結構的有效對齊) == 0;而結構 A 的有效對齊就是各個成員中有效對齊最大的那個數,也就是 b的對齊參數 8,所以 A 的有效對齊就是 8,結構的總大小要滿足對齊要求還必須在 c 後面填充 6 個字節。此時 A 的內存布局如下:

00 CC CC CC CC CCCC CC 00 00 00 00 00 00 00 00 11 11 CC CC CC CC CC CC  

而 sizeof(A) = 24;0 分別代表 a,b 的位置,1111 代表 c 的位置。

# pragma pack(4)  
struct A{  
      char a;  
      double b;  
      short c;  
};  

把指定對齊參數設置成 4,此時 a 和 c 的有效對齊仍然是其自身對齊,而 b 因為它的自身對齊 8 大於了指定的對齊 4,所以 b 的有效對齊現在變成了 4 而不再是 8 了。a 仍然位於偏移 0,b 要滿足對齊規則的話,地址偏移必須是其有效對齊的整數倍,所以 b 的偏移應該是 4,c 仍然緊緊跟在 b 的後面,因為此時的偏移 12 滿足了 c 的對齊要求12 % 2 = 0;結構 A 的有效對齊現在也變成了 4,即等於成員中最大的有效對齊,b 的有效對齊。A 的總大小要滿足對齊規則的話還必須在 c 的後面填充 2 個字節,讓總大小變為 16 字節。此時 A 的內存布局如下:

00 CC CC CC 00 0000 00 00 00 00 00 11 11 CC CC  

而 sizeof(A) = 16;0 分別代表 a,b 的位置,1111 代表c的位置。

技術分享
# pragma pack(8)  
struct A{  
      char a;  
      double b;  
};  
struct B{  
int i;  
   struct A sa;  
int c;  
};  
技術分享

我們來看結構 B 的布局,把指定對齊參數設置成 8,其實也還是沒起作用,我們最大的內部類型就是 8 的 double 了,剛好和指定的對齊參數相等。第一個成員 i 肯定是位於偏移為 0 的地址上了。然後第二個成員是一個結構成員,我們要找到這個成員的有效對齊參數,結構的有效對齊參數是其成員中最大的那個對齊參數,對於結構 A 來說就是 b 的對齊參數 8,所以 A 的有效對齊是 8。結構成員 sa 要想滿足對齊要求,即 偏移 % 有效對齊 8 = 0;它的地址偏移應該為 8。所以 sa 和 i 之間需要填充 4 個字節。成員 c 仍然緊緊跟在 sa 後面,因為 sa 占 16 字節,此時的地址偏移 24 已經可以滿足 c 的對齊要求 24 % 4 = 0 ;而結構 B 的總大小也要滿足對齊規則,B 的有效對齊就是成員中最大的,sa的有效對齊 8。所以 B 的總大小要能被 8 整除,就必須在 c 的後面再填充 4 個字節。此時結構 B 的內存布局如下:

00 00 00 00 CC CCCC CC 00 CC CC CC CC CC CC CC 00 00 00 00 00 00 00 00 11 11 11 11 CC CC CC CC  

而 sizeof(A) = 32;0 分別代表 i,sa.a,sa.b 的位置,11111111代表 c 的位置。

技術分享
# pragma pack(8)  
struct A{  
      char a;  
      double b;  
};  
struct B{  
int i;  
int c;  
   struct A sa;  
};  
技術分享

把 c 移到 sa 的上面,這樣就不需要填充任何字節了。B的所有的成員剛好滿足對齊規則。註意,結構 A 中的 b 和 a 之間還是要填充字節的,它內部要滿足自己的對齊要求。此時 B 的內存布局如下:

00 00 00 00 11 1111 11 00 CC CC CC CC CC CC CC 00 00 00 00 00 00 00 00  

而 sizeof(A) = 24;0分別代表 i ,sa.a,sa.b的位置,11111111 代表 c 的位置。

技術分享
# pragma pack(4)  
struct A{  
      char a[9];  
      double b;  
char c[29];  
int d[7];  
};  
技術分享

數組成員 a 的有效對齊是和其成員類型 char 一樣,1。成員 b 的有效對齊是指定的對齊參數 4,因為指定的比它自身的小。c 數組同樣也是 1 字節對齊,數組 d 是 4 字節對齊,指定的對齊和它自身的對齊一樣,都是 4。數組的各個成員是緊密排列的,所以,b 和 a 之間填充了 3 個字節,c 和 b之間不填充字節,d 和 c 之間填充3個字節,c 之後不填充字節。sizeof(A) = 80;

c++內存對齊 轉載