<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>

          詳聊內存對齊(Memory alignment)

          共 10767字,需瀏覽 22分鐘

           ·

          2021-07-22 10:14

          1. 同個結構體占用內存可變化

           C/C++中結構體類型,就這?章節(jié)里,對struct的功能和使用進行了詳細的說明。「內存對齊」章節(jié)作為struct的一個擴充知識。事實也證明,實際開發(fā)中,關注結構體內存布局特性的同事寥寥無幾。甚至某些同事表示從未去留意過聲明的結構體所占用內存空間大小,他們會感到詫異、驚訝,為何聲明的是同一個結構體數據類型,但是當成員列表位置排列順序進行了細微的調整(數據類型相同,成員數量保持不變)之后,占用的內存空間卻不相同。有這些疑惑的同事,我相信你在閱讀完本節(jié)內容之后,定會茅塞頓開,解開你內心的疑惑。

          請讓我們以聲明的兩個結構體數據類型 struct a  struct b 作為拉開本節(jié)的序幕。當這兩個結構體數據類型出現在你眼前時候,你是否能夠第一時間里快準狠地說出你腦海中的答案,并能夠確保它是正確無誤的。
          struct a
          {
          char c;
          int i;
          short s;
          };

          struct b
          {
          int i;
          char c;
          short s;
          };


          很顯然,這兩個數據類型里除了成員列表的排列順序有所不同外,并無任何其他差異。作為選擇題,毫無疑問它只有3個待選答案:

          ? ?? [1] 數據類型struct a占用內存空間大于數據類型struct b
          ? ?? [2] 數據類型struct a占用內存空間等于數據類型struct b
          ? ?? [3] 數據類型struct a占用內存空間小于數據類型struct b

          無論你的答案是[1],還是[2],又或許是[3],都暫時擱置片刻,因為此刻我并不著急你給出回應。與其匆忙中給出答案,我更希望的是答案推導的過程。數據類型struct a和 數據類型struct b之間的內存占用關系究竟如何?讓我們帶著這個問題繼續(xù)往下走。

          2. 結構體內存布局

          WIKI 中強調:

          The C struct directly references a contiguous block of physical memory, usually delimited (sized) by word-length boundaries. The contents of a struct are stored in contiguous memory.

          即結構體(struct)數據類型中各成員列表占用連續(xù)內存空間。現在對上面的數據類型struct a和 數據類型struct b中各成員分別在內存中的位置作一個分析。

          所有的代碼運行結果信息都是基于以下環(huán)境:

          ?? ??操作系統(tǒng):CentOS Linux release 7.5.1804 (Core) 64位
          ?? ??CPU型號:Intel? Xeon? CPU E3-1225 v3 @ 3.20GHz
          ?? ??CPU核數:cpu cores : 4

          既然結構體中各成員所占用的內存空間是連續(xù)的,那么在不考慮其他因素的情況下,數據結構struct a和數據結構struct b內存空間的占用情況大致如下圖1和圖2所示。



           數據類似struct a 內存空間分布情況

          如果數據類型struct a的內存空間分布情況真如圖1所示,那么可得知其內存大小為: 7 Byte。即 c(1Byte)+ i(4Byte) + s(2Byte)。


          數據類型struct b 內存空間分布情況

          同理數據類型 struct b的內存大小也是: 7 Btye。那么真實情況是這樣嗎?下面我們使用sizeof 操作符來打印下數據類型struct a和struct b的真實內存占用大小。

          #include <ucontext.h>#include <stdio.h>struct  a{        char c;        int i;        short s;};
          struct b{ int i; char c; short s;};int main(){ printf("sizeof(struct a) = %lu\n", sizeof(struct a)); printf("sizeof(struct b) = %lu\n", sizeof(struct b)); return 0;}

          打印結果:
          ?? ??sizeof(struct a) = 12
          ?? ??sizeof(struct b) = 8

          最終打印的真實結果和猜想有出入,且都是大于或等于各成員列表的數據類型和。現借助offsetof 宏來分別查看每個成員在該結構體內存占用中的偏移量。


          2.1 offsetof 定位某成員在結構體中的「 偏移量」

          讓我們先看下offsetof的定義,摘自 Linux Kernel 5.4.3 源碼的scripts/kconfig目錄下的list.h文件。

          ?? ??#undef offsetof
          ?? ??#define offsetof(TYPEMEMBER) ((size_t) &((TYPE *)0)->MEMBER)

          第一個參數TYPE是結構體的名字,第二個參數MEMBER是結構體成員的名字。該宏返回結構體TYPE中成員MEMBER的偏移量。偏移量是size_t類型的。


          現在使用offsetof來分別打印數據類型struct a和數據類型struct b中的各成員在各自結構體中的偏移量,同時結合內存地址來進行一個全面的分析。

          #include <ucontext.h>#include <stddef.h>#include <stdio.h>struct  a{        char c;        int i;        short s;};
          struct b{ int i; char c; short s;};int main(){ struct a a1 = {.c = 'L', .i = 26, .s = 27}; struct b b1 = {.i = 26, .c = 'X', .s = 27}; printf("\t-------------------------------------------------------------------\n"); printf("\ta1.c = %c, a1.i = %d, a1.s = %d\n", a1.c, a1.i, a1.s); printf("\ta1.c = %ld, a1.i = %ld, a1.s = %ld\n", offsetof(struct a, c), offsetof(struct a, i), offsetof(struct a, s)); printf("\ta1.c = %p, a1.i = %p, a1.s = %p\n", &(a1.c), &(a1.i), &(a1.s)); printf("\t-------------------------------------------------------------------\n"); printf("\tb1.i = %d, b1.c = %c, b1.s = %d\n", b1.i, b1.c, b1.s); printf("\tb1.c = %ld, b1.c = %ld, b1.s = %ld\n", offsetof(struct b, i), offsetof(struct b, c), offsetof(struct b, s)); printf("\tb1.c = %p, b1.c = %p, b1.s = %p\n", &(b1.i), &(b1.c), &(b1.s)); printf("\t-------------------------------------------------------------------\n"); return 0;}


          打印的結果如下圖3所示:


           數據類型struct a和struct b中各成員在結構體中內存偏移量


          上圖中紅色框標注的是各成員在結構體所占用內存中的偏移量。和預期中的圖1、圖2是有差異的。根據圖3中的打印結果重新畫一個數據類型struct a和struct b中各成員的內存分布圖。


          對于數據類型struct a, 其中成員c的偏移量是0,成員i的偏移量是4。因為成員c是char類型,所以占用1字節(jié)內存空間;那么成員c和成員i之間,有3個字節(jié)是填充字節(jié)。成員s偏移量是8, 這個沒有疑問,因為int類型占用4字節(jié)空間,成員i到s之間無填充字節(jié);數據類型struct a占用的總內存空間是12,而成員s是short int類型,占用2字節(jié),因此,成員s之后有兩個字節(jié)是填充的。內存占用分布如下圖所示。


           數據類型struct a真實內存分布

          ?? ??
          對于數據類型struct b,成員i為第一個結構體成員,且為int,占用4字節(jié)內存空間;成員c的偏移量為4,這個沒有問題。但是成員s的偏移量是6,而成員c是char數據類型,占用1字節(jié)的內存空間,那么成員c到成員s之間有一個填充字節(jié)。又因為struct b數據類型占用的總內存空間是8,則成員s(short int類型,占用2字節(jié))之后無填充字節(jié)。內存占用分布如下圖所示。


           數據類型struct b真實內存分布

          到這里時,難免有些疑問,為何結構體占用的內存空間會大于或等于各成員列表的數據類型之和呢?為什么會有填充字節(jié)存在?誰負責填充?什么時候填充?有何作用?想要解開這系列的疑惑,就得知道「內存對齊」原理和作用。
          ?? ??

          2.2 為保證內存對齊,填充了什么值

          填充位的值是未定義的,尤其是不能保證它們會歸零。老派的說法是"slop"。

          3. 內存對齊

          Linux開發(fā)同事應該會很清楚這樣一個事實,運行的成果物是經過編譯、鏈接之后生成的ELF格式的目標文件。當程序運行起來時候,系統(tǒng)會去讀取ELF文件,并將文件中的數據(地址、代碼段、數據段、符號表鏈接等等)信息加載到系統(tǒng)的內存中;程序會按照事先指定的條件去不斷運行。在這個過程中,CPU會不斷地去從內存條某地址上面不斷的來回讀取數據到寄存器上面參與運算。當計算機讀取或寫入內存地址時,它將以字(word)大小的塊進行存儲。數據對齊意味著將數據放在等于字長的倍數的內存偏移處,這由于CPU處理內存的方式而提高了系統(tǒng)的性能。大多數CPU只能訪問內存對齊的地址。


          Microchip 中對于內存對齊就作了如下說明:
          由于PIC32MZ存儲器系統(tǒng)為32位寬,因此可以對齊或不對齊大小為32位(4字節(jié)或WORD)或16位(2字節(jié)或半字)的數據訪問:

          • (自然)對齊:數據的地址值是數據類型大小的倍數(以字節(jié)為單位)

              i. WORD大小傳輸執(zhí)行到4的倍數的地址:0x00000000、0x00000004、0x00000008, …
              ii. 半字大小傳輸執(zhí)行到2的倍數的地址:0x00000000、0x00000002、0x0000000, …
          • 不對齊傳輸:表示數據地址不符合上述規(guī)則。


          在看完Microchip中對于“內存對齊”的描述說明后,是不是豁然開朗。不急,再看下關于“內存對齊”的一段正式定義:

          關于內存對齊,有這樣一段正式定義:

          A memory address a, is said to be n-byte aligned when n is a power of two and a is a multiple of n bytes. In this context a byte is the smallest unit of memory access, i.e. each memory address specifies a different byte. An n-byte aligned address would have log2 n least-significant zeros when expressed in binary. 


          A memory access is said to be aligned when the data being accessed is n bytes long and the address is n-byte aligned. When a memory access is not aligned, it is said to be misaligned or Non Aligned. Note that by definition byte memory accesses are always aligned.

          當n是2的冪且a是n字節(jié)的倍數時,內存地址a被稱為n字節(jié)對齊。在這種情況下,字節(jié)是存儲器訪問的最小單位,即每個存儲器地址指定一個不同的字節(jié)。當以二進制表示時,一個n字節(jié)對齊的地址將具有log2的n次方個最低有效零。當正在訪問的數據為n字節(jié)長且地址為n字節(jié)對齊時,則稱內存訪問已對齊。如果內存訪問未對齊,則稱其為未對齊或未對齊。請注意,根據定義,字節(jié)存儲器訪問總是對齊的。

          內存對齊(Memory alignment)概念的引入,基于兩個條件:

          (1) 某些處理器不支持訪問位于不對齊內存地址上面的數據,會產生異常事件。
          (2) 訪問非對齊內存地址上面的數據,會降低CPU的性能。因為如果沒有對齊約束,代碼可能最終不得不跨越機器字邊界進行兩次或多次訪問。

          Daniel Drake、Johannes Berg對于「Unaligned Memory Accesses」未對齊內存訪問 就提出了以下幾點說明:

          執(zhí)行未對齊內存訪問的效果因體系結構而異。在這里,很容易就可以寫出一份關
          于這些差異的完整的文檔;常見的情況摘要如下:

          1. 有些體系結構能夠透明地執(zhí)行未對齊的內存訪問,但通常會有很大的性能開銷。
          2. 有些體系結構在發(fā)生未對齊訪問時引發(fā)處理器異常。異常處理程序能夠更正未對齊的訪問,但會大大降低性能。
          3. 有些體系結構在發(fā)生未對齊訪問時引發(fā)處理器異常,但這些異常不包含足夠的信息,無法更正未對齊訪問。
          4. 有些架構無法進行未對齊的內存訪問,但是會靜默地對請求的內存執(zhí)行另一種內存訪問,從而導致難以檢測的細微代碼錯誤!

          可想而知,訪問未對齊的內存地址是多么的糟糕!!!

          DevX.com 中強調:大多數CPU要求對象和變量位于系統(tǒng)內存中的特定偏移處。例如,32位處理器需要一個4字節(jié)的整數來駐留在可被4整除的內存地址上。此要求稱為“內存對齊”。因此,一個4字節(jié)的int可以位于內存地址0x2000或0x2004處,而不是0x2001。在大多數Unix系統(tǒng)上,嘗試使用未對齊的數據會導致總線錯誤,從而完全終止程序。在Intel處理器上,支持使用未對齊的數據,但會大大降低性能。因此,大多數編譯器會根據其類型和所使用的特定處理器自動對齊數據變量。這就是為什么結構和類占用的大小通常大于其成員大小之和的原因。


          3.1 結構體成員默認內存對齊

          如下表1為結構體成員在不同編譯器環(huán)境下的默認內存對齊方式。編譯器將根據需要在成員之間插入未使用的字節(jié),以獲得此對齊方式。編譯器還將在結構的末尾插入未使用的字節(jié),以使結構的總大小是需要最高對齊的元素的對齊的倍數。許多編譯器都有更改默認對齊方式的選項。結構成員對齊方式的不同將導致訪問相同數據的不同程序或模塊之間的不兼容性,以及數據何時存儲在二進制文件中。我們可以通過對結構成員進行排序來避免此類兼容性問題,從而無需插入未使用的字節(jié)。同樣,可以通過插入所需大小的虛擬成員來明確指定結構末尾的填充。虛擬表指針的大小(如果有)必須考慮在內。


           表1 結構和類數據成員字節(jié)對齊方式?? ??
          ?? ??

          3.2 不同架構內存對齊方式

          表2描述了一些內存地址在不同架構平臺上面的內存對齊方式,可供參考。


           表2 不同架構上的內存對齊


          要找出運行現代UNIX的處理器的自然字長,可以使用以下命令:

          • getconf WORD_BIT

          • getconf LONG_BIT

          對于現代的x86_64計算機,WORD_BIT將返回32,LONG_BIT將返回64。對于沒有64位擴展的x86計算機,這兩種情況下都是32位。


          ?? ??

          3.3 編譯器了解「對齊約束 」

          盡管某些處理架構對訪問未對齊的內存地址數據會產生異常現象,但是平時開發(fā)中卻幾乎碰不到。這是因為編譯器了解對齊約束規(guī)則,在背后會對聲明的結構體數據類型作檢測,若成員列表不滿足對齊規(guī)則,則會默認填充字節(jié)滿足規(guī)則。即當訪問N個字節(jié)的內存時,基本內存地址必須被N整除,即addr%N ==0。正如前面聲明的數據類型:struct a。

          3.3.1 單個字節(jié)永不會導致未對齊內存訪問

          訪問單個字節(jié)(unsigned char或char)將永遠不會導致未對齊的訪問,因為所有內存地址都可以被一個整數整除。如:

          ?? ??typedef struct SingleChar
          ?? ??{
          ?? ???? ??char a;
          ?? ??}SingleCh;

          則數據類型SingleCh大小將永遠是為1。

          4. 小試牛刀

          通常,結構實例將具有其最寬標量成員的對齊方式,編譯器這樣做是確保所有成員都能自對齊以快速訪問的最簡單方法。如下定義:


          結構總是與最大類型的對齊需求保持一致。

          換言之,它等同于下面的定義。


          因此,對于下面的結構數據類型聲明 struct Test 。 則 Kmax = 4,取決于int類型,因為在該數據類型的成員列表中,int數據類型最大。 則 Kmax = 8,取決于char*類型,因為在該數據類型的成員列表中,char*指針數據類型最大。
          ?? ??
          示例一??

          #include <stdio.h>#include <stdlib.h>#include <string.h>
          struct Test{ char a; char *p; int c;};
          int main(){ struct Test t; printf("sizeof(t) = %ld\n", sizeof(t)); return 0;}
          數據類型struct Test占用的內存空間大小是:24字節(jié)。
          ?? ??struct Test{
          ?? ??    char a;
          ?? ??    //char pad[7] 填充7字節(jié)
          ?? ??    char *p;
          ?? ??    int c;
          ?? ??    //char pad[4] 填充4字節(jié)
          ?? ??};

           ??

          示例二

          #include <stdio.h>#include <string.h>#include <stdlib.h>
          struct a{ int a; char b; short c;};int main(){ printf("sizeof(struct a) = %ld\n", sizeof(struct a)); return 0;}
          該結構體類型占用的空間大小是:8字節(jié)。

          ?? ??struct a
          ?? ??{
          ?? ???? ?? int a;
          ?? ???? ??char b;
          ?? ???? ??//char pad[1] 填充1字節(jié)
          ?? ???? ??short c;
          ?? ??};



          當結構體內部嵌套其他結構體時候,同樣會有內存對齊規(guī)則。
          #include <stdio.h>#include <string.h>#include <stdlib.h>
          struct a{ char c; struct b{ char *p; short d; }b1;};int main(){ printf("sizeof(struct a) = %ld\n", sizeof(struct a)); return 0;}

          占用空間大小為:24字節(jié)。
          ?? ??struct a
          ?? ??{
          ?? ???? ??char c;
          ?? ???? ??//char pad[7] 填充7字節(jié)
          ?? ???? ??struct b{
          ?? ???? ???? ??char *p;
          ?? ???? ???? ??short d;
          ?? ???? ???? ??//char pad[6] 填充6字節(jié)
          ?? ???? ??}b1;
          ?? ??};


          示例三

          #include <stdio.h>#include <stdlib.h>#include <string.h>
          struct Test{ char *p; char a;};int main(){ struct Test t; struct Test t1[4]; printf("sizeof(t) = %ld\n", sizeof(t)); printf("sizeof(t1) = %ld\n", sizeof(t1));
          return 0;}

          在64位系統(tǒng)上面,指針數據類型占8字節(jié)。因此,該數據類型占用的內存空間大小是:16字節(jié)。
          ?? ??struct Test{
          ?? ??char *p; ?? ?? ?? ? //8Btye
          ?? ??char a; ?? ?? ?? ?? //1Byte
          ?? ?? //char pad[7] ? ? //填充7Byte
          ?? ??};


          ?? ??
          ?? ??
          示例四
          struct S2 {
          double v;
          int i[2];
          char c;
          };

          數據類型 struct S2中,Kmax = 4,因此成員列表中各成員按照4字節(jié)內存對齊。


          ?? ??? ??

          5. 禁止編譯器因內存對齊而進行內存填充

          在前面有反復提及過,盡管聲明的結構體數據類型本身各成員列表不具備內存對齊條件,但是編譯器會進行合適字節(jié)填充,以為了達到內存對齊的條件。有了默認填充規(guī)則,當然也有顯示禁止填充的條件;如果你十分確認你的處理架構能夠訪問、讀寫不對齊的內存地址,且不會帶來效率降低的潛在風險。你可以在項目中顯示的告知編譯器不要進行內存對齊而填充其他字段。很顯然,這樣帶來了內存空間的節(jié)約;但是可移植差,具有隱藏BUG。

          If desired, it’s actually possible to prevent the compiler from padding a struct using either __attribute__((packed)) after a struct definition, #pragma pack (1) in front of a struct definition or -fpack-struct as a compiler parameter. It’s important to note that using either of these will generate an incompatible ABI. We can use the sizeof operator to check the effective size of a struct and output it
          during runtime using printf.

          大致上就是說:“如果需要,實際上可以防止編譯器在結構定義后使用_attribute__((packed))填充結構,或者在結構定義前使用#pragma pack(1),或者使用-fpack-struct作為編譯器參數。需要注意的是,使用這兩種方法都會生成不兼容的ABI。我們可以使用sizeof操作符來檢查結構的有效大小,并在運行時使用printf輸出它。”
          ?? ??
          示例一
          struct{    char a;#if 0    int b __attribute__((packed));#else    int b;#endif}a;

          若在成員b后面添加__attribute__((packed)) ,則結構體大小是5;反之則是8.
          ?? ??

          示例二

          gcc 文件名.c -fpack-struct

          使用該GNU編譯參數能夠達到和示例一同樣的效果。 ??

          《深入Linux內核架構》一書中的附錄 -“附錄a.3 對齊” , 也對內存對齊進行了描述。如下圖:


          瀏覽 174
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  永久免费一区二区三区 | 操屄视频网 | 午夜剧场污 | 五月天无码在线 | 成人摸在线 |