面試常考,專案易錯,長文詳解CC++中的位元組對齊
引入主題,看程式碼
我們先來看看以下程式
#include 編譯的結果如下: 問題來了,兩個結構體的內容一樣,只是換了個位置,為什麼 sizeof(st) 的時候大小不一樣呢? 沒錯,這正是因為記憶體對齊的影響,導致的結果不同。對於我們大部分程式設計師來說,都不知道記憶體是怎麼分佈的。 實際上因為這是編譯器該乾的活,編譯器把程式中的每個資料單元安排在合適的位置上,導致了相同的變數,不同宣告順序的結構體大小的不同。 如果看文章嫌太長,可以直接轉接影片教程,《什麼是結構體記憶體對齊?C語言零基礎教程之結構體記憶體對齊》 幾種型別資料所佔位元組數 int,long int,short int的寬度和機器字長及編譯器有關,但一般都有以下規則(ANSI/ISO制訂的) sizeof(short int) <= sizeof(int) sizeof(int) <= sizeof(long int) short int 至少應為16位(2位元組) long int 至少應為32位 資料型別16位編譯器32位編譯器64位編譯器 char1位元組1位元組1位元組 char*2位元組4位元組8位元組 short int2位元組2位元組2位元組 int2位元組4位元組4位元組 unsigned int2位元組4位元組4位元組 float4位元組4位元組4位元組 double8位元組8位元組8位元組 long4位元組4位元組8位元組 long long8位元組8位元組8位元組 unsigned long4位元組4位元組8位元組 什麼是對齊 現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問都可以從任何地址開始,但實際情況是在訪問 特定變數 的時候經常在 特定的記憶體地址 訪問。 所以這就需要各型別資料 按照一定的規則在空間上排列 ,而不是順序的一個接一個的排放,這就是對齊。 記憶體對齊又分為自然對齊和規則對齊 。 對於記憶體對齊問題,主要存在於struct和union等複合結構在記憶體中的分佈情況,許多實際的計算機系統對基本型別資料在記憶體中存放的位置有限制,它們要求這些資料的首地址的值是某個數M(通常是4或8); 對於記憶體對齊,主要是為了提高程式的效能,資料結構,特別是棧,應儘可能在自然邊界上對齊,經過對齊後,cpu的記憶體訪問速度大大提升。 自然對齊 指的是將對應變數型別存入對應地址值的記憶體空間,即資料要根據其資料型別存放到以其資料型別為倍數的地址處。 例如char型別佔1個位元組空間,1的倍數是所有數,因此可以放置在任何允許地址處,而int型別佔4個位元組空間,以4為倍數的地址就有0,4,8等。編譯器會優先按照自然對齊進行資料地址分配。 規則對齊 以結構體為例就是在自然對齊後,編譯器將對自然對齊產生的空隙記憶體填充無效資料,且填充後結構體佔記憶體空間為結構體內佔記憶體空間最大的資料型別成員變數的整數倍。 實驗對比 首先看這個結構體 typedef struct test_32{ char a; short b; short c; char d;}test_32; 首先按照自然對齊,得到如下圖的記憶體分佈位置,第一個格子地址為0,後面遞增。 編譯器將對空白處進行無效資料填充,最後將得到此結構體佔記憶體空間為8位元組,這個數值也是最大的資料型別short的2個位元組的整數倍。 如果稍微調換一下位置的結構體 typedef struct test_32{ char a; char b; short c; short d;}test_32; 同樣按照自然對齊如下圖分佈 可以看到按照自然對齊,變數之間沒有出現間隙,所以規則對齊也不用進行填充,而這裡有顏色的方格有6個,也就是6個位元組 按照規則對齊,6位元組是此結構體中最大資料型別short的整數倍,因此此結構體為6位元組,後面的空白不需理會,可以實際編譯一下執行,結果和分析一致為6個位元組。 double的情況 我們知道32位處理器一次只能處理32位也就是4個位元組的資料,而double是8位元組資料型別,這要怎麼處理呢? 如果是64位處理器,8位元組資料可以一次處理完畢,而在32位處理器下,為了也能處理double8位元組資料,在處理的時候將 會把double拆分成兩個4位元組數進行處理 ,從這裡就會出現一種情況如下: typedef struct test_32{ char a; char b; double c;}test_32; 這個結構體在32位下所佔記憶體空間為12位元組,只能拆分成兩個4位元組進行處理,所以這裡規則對齊將判定該結構體最大資料型別長度為4位元組,因此總長度為4位元組的整數倍,也就是12位元組。 這個結構體在64位環境下所佔記憶體空間為16位元組,而64位判定最大為8位元組,所以結果也是8位元組的整數倍:16位元組。這裡的結構體中的double沒有按照自然對齊放置到理論上的8位元組倍數地址處,我認為這裡編譯器也有根據規則對齊做出相應的最佳化,節省了4個多餘位元組。 這部分各位可以按照上述規則自行分析測試。 陣列 對齊值為: min(陣列元素型別,指定對齊長度) 。但陣列中的元素是連續存放,存放時還是按照陣列實際的長度。 如char t[9],對齊長度為1,實際佔用連續的9byte。然後根據下一個元素的對齊長度決定在下一個元素之前填補多少byte。 巢狀的結構體 假設 struct A{ …… struct B b; ……}; 對於B結構體在A中的對齊長度為: min(B結構體的對齊長度,指定的對齊長度) 。 B結構體的對齊長度為:上述2種結構整體對齊規則中的對齊長度。 舉個例子 #include 輸出結果: 改成#pragma pack (16)結果一樣,這個例子證明了三點: 對齊長度長於struct中的型別長度最長的值時,設定的對齊長度等於無用 陣列對齊的長度是按照陣列成員型別長度來比對的 巢狀的結構體中,所包含的結構體的對齊長度是結構體的對齊長度 指標 主要是因為32位和64位機定址上,來看看例子 //編譯器:https://tool。lu/coderunner///來源:技術讓夢想更偉大//作者:李肖遙#include 結果如下 pack48 length3240 args188 args248 記憶體對齊的規則 資料成員對齊規則 結構或聯合的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員的對齊按照 #pragma pack 指定的數值和這個資料成員自身長度中,比較小的那個進行。 例如struct a裡存有struct b,b裡有char,int ,double等元素,那b應該從8的整數倍開始儲存。 結構體作為成員 如果一個結構裡有某些結構體成員,則結構體成員要從其內部“最寬基本型別成員”的整數倍地址開始儲存。 在資料成員完成各自對齊之後,結構或聯合本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構或聯合最大資料成員長度中,比較小的那個進行。 1&2的情況下注意 當#pragma pack的n值等於或超過所有資料成員長度的時候,這個n值的大小將不產生任何效果。 #pragma pack()用法詳解 作用 指定結構體、聯合以及類成員的packing alignment; 語法 #pragma pack( [show] | [push | pop] [, identifier], n ) 說明 pack提供資料宣告級別的控制,對定義不起作用; 呼叫pack時不指定引數,n將被設成預設值; 一旦改變資料型別的alignment,直接效果就是佔用memory的減少,但是performance會下降; 語法具體分析 show:可選引數 顯示當前packing aligment的位元組數,以warning message的形式被顯示; push:可選引數 將當前指定的packing alignment數值進行壓棧操作,這裡的棧是the internal compiler stack,同時設定當前的packing alignment為n;如果n沒有指定,則將當前的packing alignment數值壓棧; pop:可選引數 從internal compiler stack中刪除最頂端的record;如果沒有指定n,則當前棧頂record即為新的packing alignment數值;如果指定了n,則n將成為新的packing aligment數值;如果指定了identifier,則internal compiler stack中的record都將被pop直到identifier被找到,然後pop出identitier,同時設定packing alignment數值為當前棧頂的record;如果指定的identifier並不存在於internal compiler stack,則pop操作被忽略; identifier:可選引數 當同push一起使用時,賦予當前被壓入棧中的record一個名稱;當同pop一起使用時,從internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier沒有被找到,則忽略pop操作; n:可選引數 指定packing的數值,以位元組為單位;預設數值是8,合法的數值分別是1、2、4、8、16 例子 #include 從執行結果來看我們可以證實上面記憶體對齊規則的第一條:第一個資料成員放在offset為0的地方。 現在咱來看看上面結構體是如何記憶體對齊的;先用程式碼列印它們每個資料成員的儲存地址的偏移量 #include 在此c在結構體中偏移量為8加上它自身(int)4個位元組,剛好是12(c的開始位置為8,所以要加它的4個位元組) 上面記憶體結束為11,因為0-11,12是最大對齊數的整數倍,故取其臨近的倍數,所以就取4的整數倍即12; 上圖中我用連續的陣列來模仿記憶體,如圖是它們的記憶體對齊圖; 如果將最大記憶體對齊數改為8,它將驗證記憶體對齊規則中的第3條。 如果將其改為2,會發生什麼:我們來看看: #include 對於這個結果,我們按剛才第一個例子我所分析的過程來分析這段程式碼,得到的是10; 故當我們將#pragma pack的n值小於所有資料成員長度的時候,結果將改變。 對齊的作用和原因 各個硬體平臺對儲存空間的處理上有很大的不同。如果不按照適合其平臺要求對資料存放進行對齊,可能會在存取效率上帶來損失。 比如有些平臺每次讀都是從偶地址開始,如果一個int型在32位地址存放在偶地址開始的地方,那麼一個讀週期就可以讀出; 而如果存放在其地址開始的地方,就可能會需要2個讀週期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該int資料。那麼在讀取效率上下降很多,這也是空間和時間的博弈。 CPU每次從記憶體中取出資料或者指令時,並非想象中的一個一個位元組取出拼接的,而是根據自己的字長,也就是CPU一次能夠處理的資料長度取出記憶體塊。總之,CPU會以它“最舒服的”資料長度來讀取記憶體資料 舉個例子 如果有一個4位元組長度的指令準備被讀取進CPU處理,就會有兩種情況出現: 4個位元組起始地址剛好就在CPU讀取的地址處,這種情況下,CPU可以一次就把這個指令讀出,並執行,記憶體情況如下 而當4個位元組按照如下圖所示分佈時 假設CPU還在同一個地址取資料,則取到第一個4位元組單元得到了1、2位元組的資料,但是這個資料不符合需要的數字,所以CPU就要在後續的記憶體中繼續取值,這才取到後面的4位元組單元得到3、4位元組資料,從而和前面取到的1、2位元組拼接成一個完整資料。 而本次操作進行了兩次記憶體讀取,考慮到CPU做大量的資料運算和操作,如果遇到這種情況很多的話,將會嚴重影響CPU的處理速度。 因此,系統需要進行記憶體對齊,而這項任務就交給編譯器進行相應的地址分配和最佳化,編譯器會根據提供引數或者目標環境進行相應的記憶體對齊。 什麼時候需要進行記憶體對齊。 一般情況下都不需要對編譯器進行的記憶體對齊規則進行修改,因為這樣會降低程式的效能,除非在以下兩種情況下: 這個結構需要直接被寫入檔案 這個結構需透過網路傳給其他程式 對齊的實現 可以通知給編譯器傳遞預編譯指令,從而改變對指定資料的對齊方法。 unsigned int calc_align(unsigned int n,unsigned align) { if ( n / align * align == n) return n; return (n / align + 1) * align; } 不過這種演算法的效率很低,下面介紹一種高效率的資料對齊演算法: unsigned int calc_align(unsigned int n,unsigned align) { return ((n + align - 1) & (~(align - 1))); } 這種演算法的原理是: (align-1) :對齊所需的對齊位,如:2位元組對齊為1,4位元組為11,8位元組為111,16位元組為1111。。。 (&~(align-1)) :將對齊位資料置位為0,其位為1 (n+(align-1)) & ~(align-1) :對齊後的資料 總結 通常,我們寫程式的時候,不需要考慮對齊問題,編譯器會替我們選擇目標平臺的對齊策略。但正因為我們沒注意這個問題,導致編輯器對資料存放做了對齊,而我們如果不瞭解的話,就會對一些問題感到迷惑。所以知其然,更要知其所以然。好了,我們介紹到這裡,下一期再見! 客觀請留步 如果你基礎比較差,正好在學習C/C++,看文章比較無聊,不妨關注下關注下小編的影片教程,通俗易懂,深入淺出,一個影片只講一個知識點。影片不深奧,不需要鑽研,在公交、在地鐵、在廁所都可以觀看,隨時隨地漲姿勢