内存地址对齐提升程序性能
內存地址對齊,是一種在計算機內存中排列數據、訪問數據的一種方式,包含了兩種相互獨立又相互關聯的部分:基本數據對齊和結構體數據對齊。當今的計算機在計算機內存中讀寫數據時都是按字(word)大小塊來進行操作的(在32位系統中,數據總線寬度為32,每次能讀取4字節,地址總線寬度為32,因此最大的尋址空間為2^32=4GB,但是最低2位A[0],A[1]是不用于尋址,A[2-31]才能存儲器相連,因此只能訪問4的倍數地址空間,但是總的尋址空間還是2^30*字長=4GB,因此在內存中所有存放的基本類型數據的首地址的最低兩位都是0,除結構體中的成員變量)?;绢愋蛿祿R就是數據在內存中的偏移地址必須等于一個字的倍數,按這種存儲數據的方式,可以提升系統在讀取數據時的性能。為了對齊數據,可能必須在上一個數據結束和下一個數據開始的地方插入一些沒有用處字節,這就是結構體數據對齊。
?
????? 舉個例子,假設計算機的字大小為4個字節,因此變量在內存中的首地址都是滿足4地址對齊,CPU只能對4的倍數的地址進行讀取,而每次能讀取4個字節大小的數據。假設有一個整型的數據a的首地址不是4的倍數(如下圖所示),不妨設為0X00FFFFF3,則該整型數據存儲在地址范圍為0X00FFFFF3~0X00FFFFF6的存儲空間中,而CPU每次只能對4的倍數內存地址進行讀取,因此想讀取a的數據,CPU要分別在0X00FFFFF0和0X00FFFFF4進行兩次內存讀取,而且還要對兩次讀取的數據進行處理才能得到a的數據,而一個程序的瓶頸往往不是CPU的速度,而是取決于內存的帶寬,因為CPU得處理速度要遠大于從內存中讀取數據的速度,因此減少對內存空間的訪問是提高程序性能的關鍵。從上例可以看出,采取內存地址對齊策略是提高程序性能的關鍵。
??????????????????
?
????? 結構體(struct)是C語言中非常有用的用戶自定義數據類型,而結構體類型的變量以及其各成員在內存中的又是怎樣布局的呢?怎樣對齊的呢?很顯然結構體變量首地址必須是4字節對齊的,但是結構體的每個成員有各自默認的對齊方式,結構體中各成員在內存中出現的位置是隨它們的聲明順序依次遞增的,并且第一個成員的首地址等于整個結構體變量的首地址。下面列出了在Microsoft,Borland,GNU上對于X86架構32位系統的結構體成員各種類型的默認對齊方式。
char(1字節),1字節對齊
short(2字節),2字節對齊
int(4字節),4字節對齊
float(4字節),4字節對齊
double(8字節),Windows系統中8字節對齊,Linux系統中4字節對齊
?
????? 當結構體某一成員后面緊跟一個要求比較大的地址對齊成員時(例如char成員變量后面跟一個double成員變量),或是在,這時要插入一些沒有實際意義的填充(Padding)。而且總的結構體大小必須為最大對齊的倍數。
?
???? 下面是一個有char,int,short三種類型,4個成員組成的結構體,該結構體在還未編譯之前是大小占8個字節。
struct AlignData{??? char a;??? short b;??? int c;??? char d;};編譯之后,為了保持結構體中的每個成員都是按照各自的對齊,編譯器會在一些成員之間插入一些padding,因此編譯后得到如下的結構體:struct AlignData {??? char a;??? char Padding0[1];???? short b;??? int c;????? char d;??? char Padding1[3]; };編譯后該結構體的大小為12個字節,最后一個成員d后面填充的字節數要使該結構體的總大小是其成員類型中擁有最大字節數的倍數(int擁有最大字節數),因此d后面要填充3個字節。下面舉一些結構體例子來說明結構體的填充方式:例子1:struct struct1{??? char a1;??? char b1;};結構體struct1的大小為2字節,因為char在結構體中的默認對齊是1,因此在a1和b1之間沒有數據填充,而且其成員中占用字節最大的類型為char,因此結構體結束處和b1之間也沒有數據填充。例子2:struct struct2{??? char a2;??? short b2;};結構體struct2的大小為4字節,b2的是按2字節對齊,因此在b2于a2之間填充一個字節,而其成員中占用字節最大的類型為short,因此該結構體結束處和b2之間沒有任何數據填充。例子3:Struct struct3{double a3;char b3;}結構體struct3的大小為16字節,因為b3是按1字節對齊,所以b3與a3之間沒有數據填充,而其成員中占用字節最大的類型為double,在Windows平臺下是8字節對齊,因此該結構體結束處和b3之間有7個字節的數據填充。 填充字節的大小和新的偏移地址有如下計算公式:
padding = (align - (offset mod align)) mod align
new offset = offset + ((align - (offset mod align)) mod align)
例如求成員a,b之間的填充字節,b的默認對齊為align=2個字節,b的未填充之前的偏移量offset=1,因此填充字節數padding=(2-(1 mod 2)) mod 2 = 1字節。如果要算接下來的成員之間的填充數,已經填充的字節也要算上,不然在算偏移量的時候會出錯編譯后的結構體比未編譯之前多出了3個字節,有沒有什么辦法可以在保持各成員地址對齊的前提下,又能減少結構體的大小?答案是肯定的!如果把struct AlignData的成員順序調整成如下形式:
struct AlignData{??? char a;???? char d;??? short b;??? int c;};
那么編譯后不用填充字節就能保持所有的成員都按各自默認的地址對齊。這樣可以節約不少內存!一般的結構體成員按照默認對齊字節數遞增或是遞減的順序排放,會使總的填充字節數最少?;緮祿愋蛿到M在內存中的布局并不是每個數組的元素都是按照4字節對齊的,但是數組的首地址必須是按照4字節對齊,而且每個元素之間沒有填充,為什么沒有填充呢?地址對齊和填充的目的是減少內存讀取的次數,但現在只要數組的首地址按4字節對齊,任何小于等于4字節的類型數組(char, short, int)中的任意數組元素都能通過一次內存讀取來獲得(假設該數據沒有加載到高速緩存),任何大于4字節類型數組(double)中的任意數組元素都能通過兩次內存讀取來獲得。因此要求每個數組元素都是按照4字節對齊是沒有必要,浪費空間的。結構體數組在內存中的布局,只要保持結構體數組的首地址是按照4字節對齊,而且每個數組元素同樣也不必按照4字節地址對齊,就能盡量使內存的讀取次數降到最低,因為只要每個結構體元素自己內部的填充和對齊都是上述的方式,那么同樣也能達到既能減少內存訪問的次數,又能節約不必要的內存浪費。但是有人會有這樣的疑問,既然每個結構體首地址按照4字節對齊,為什么結構體內部每種數據類型還要各自默認的對齊大小進行對齊?其實其目的同樣也是減少內存訪問的次數,因為結構體是用戶自定義的類型,內部還是由一些基本數據類型組成的!以上的對齊方式都是Windows默認的對齊方式,用戶可以根據需求來設置自己的對齊方式,特別是在一些內存受限的系統中,內存比速度更重要!但是建議用戶還是不要輕易來設置自己的對齊方式,如果用得不恰當的話,可能會造成大量冗余的內存讀取,而且可能會出現不兼容的問題??梢杂?pragma pack指令來對其進行設置,具體的用法請參考[1,2]。 由內存地址對齊而引發的對減少內存訪問次數的思考當今的CPU的處理速度遠比內存訪問的速度快,程序的執行速度的瓶頸往往不是CPU的處理速度不夠,而是內存訪問的延遲,雖然當今CPU中加入了高速緩存用來掩蓋內存訪問的延遲,但是如果高密集的內存訪問,一種延遲是無可避免的。內存地址對齊給程序帶來了很大的性能提升,在windows等系統了,編譯器都提供了自動地址對齊,給程序員帶來了很大的方便。但是減少對內存訪問還是值得探討的問題。調整結構體成員變量的布局是減少內存訪問次數的途徑之一。下面分別介紹兩種不同的結構體數據成員調整方案,都能得到很好的性能提升。1.?? 按成員內存對齊大小按升序或是降序排序,減少結構體的大小??慈缦聝蓚€結構體:struct BeforeAdjust
{
??? char a;
??? short b;
??? int c;
??? char d;
};
?
struct AfterAdjust
{
??? char a;
??? char d;
??? short b;
??? int c;
};
從表面上看結構體BeforeAdjust和AfterAdjust成員都一樣,就是成員布置的順序有差異,因此造成了這兩種類型數據占據空間大小有所不同,BeforeAdjust大小占12個字節,AfterAdjust大小占8個字節,因此從讀取一個BeforeAdjust類型的數據要進行3次內存讀取操作,而AfterAdjust類型的數據要進行2次內存讀取操作。下面我分別對大小為1000萬的這兩種結構體的動態數組進行初始化,然后依次讀取數組數據對每個數據成員做求和操作,得到的測試時間如下表。操作 耗時(ms) 10^7個UnMergeMember數據初始化和求和操作 510.289ms 10^7個MergeMember數據初始化和求和操作 398.266ms 性能提高 28.127%
從上面的測試數據可以看出,同樣的數據成員,就是因為擺放的順序不同而造成性能有28.127%的差異。因此調整好結構體內的數據成員的擺放順序既可以減少內存的使用,又可以提高程序的性能。2.?? 把一些字節數占用比較少的成員合并到字節數占用大的成員。首先看如下兩個結構體:struct UnMergeMember
{
??? int a;
??? int b;
??? char c;
};
?
struct MergeMember
{
??? int a;
??? union
??? {
??????? int b;
?????? char c;
??? };
???
};UnMergeMember結構體由三個成員變量a,b,c,分別是int, int, char類型,按照地址對齊的規則,該結構體占用12個字節。因此初始化UnMergeMember類型變量涉及到3次內存讀操作,3次賦值操作,3次內存寫操作。MergeMember結構體由一個int類型的成員和一個聯合體變量組成,按照地址對齊規則,該結構體占用8個字節。聯合體union{int b; char c;}占用4個字節,高位3個字節保存變量b(前提是用3字節能足夠表示b的數據范圍),最低位1個字節保存變量c。假設定義一個MergeMember類型的變量為merge,初始化每個成員變量如下:merge.a = some integer;merge.b = some integer;merge.b <<= 8;merge.c = some char;初始化一個MergeMember類型的數據只涉及到2次的內存讀操作、3次賦值操作、1次位移操作,2次內存寫操作。從上述可以看出初始化一個UnMergeMember類型的變量比MergeMember類型變量多了1次讀操作和寫操作,少了1次位移操作。下面我分別對大小為1000萬的這兩種結構體的動態數組進行初始化,然后依次讀取數組數據對每個數據成員做求和操作,得到的測試時間如下表。操作 耗時(ms) 10^7個UnMergeMember數據初始化和求和操作 507.537ms 10^7個MergeMember數據初始化和求和操作 353.67ms 性能提高 43.5%
從上面的測試數據可以看出,在結構體中把小數據歸并到大數據可以減少內存讀取的次數,雖然多了一些CPU的操作,但是用CPU的操作換取內存數據讀取次數,程序性能肯定能得到提高。上面的測試程序可以得到43.5%的性能提高(基本等于內存讀取次數減少比(6-4)/4=50%),在對性能要求特別高的系統中,這么大幅度的性能是相當可觀。上述的例子也可以通過位段實現(Bit-fields),但是位段只能對整數進行操作,如果把浮點數于和int類型的數據放在一起用位段實現顯然不行,但是通過位移的方法也可以把char類型的數據并入浮點數float或是double中。3.??? 通過位段(Bit-fields)的方式把一些整形數據按照各自需求的字段數來分配。這種方式可以大大節省空間,TCP協議的首部的定義就是采用位段的方式來定義的。位段的使用比較簡單,這里我就不贅述了,可以參考相關的資料。但是值得注意的是,使用位段的方式的對齊方式也要遵守上述結構體對齊的方式,看下面一些結構體以及相應的大小:struct BitField1
{
?? char a:1;
??? char b:2;
??? char c:3;
??? char d:2;
};
struct BitField2
{
?? char a:1;
??? char b:2;
??? char c:3;
??? char d:2;
?? int e:4;
}Sizeof(BitField1)等于1(char大小的倍數),sizeof(BitField2)等于8(int大小的倍數)。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/donkeylong/archive/2009/12/01/4909720.aspx
轉載于:https://www.cnblogs.com/azraelly/archive/2012/12/31/2840479.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的内存地址对齐提升程序性能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CnCommon 的一些通用函数说明
- 下一篇: Win7输入法消失和不能切换的办法了