目錄
內存對齊
計算機的內存空間都是按照字節劃分的,元素(包括:變量、結構體成員、共用體成員)會按照定義的順序一個一個放到內存中。從理論上講,似乎可以從任意地址開始存儲任何類型的元素,但實際上元素之間並不是緊密排列的。因爲計算機系統對於基本數據類型在內存中的存放位置是有限制的:假設一個變量佔用 n 個字節,則該變量的起始地址必須能夠被 n 整除(起始地址 % n = 0)。
例 1:
#include<stdio.h>
struct {
char x;
int y;
char z;
} Test;
int main()
{
printf("%d\n", sizeof(Test));
return 0;
}
執行結果爲:12。
下圖爲上例的內存佈局圖,有以下幾個關鍵點:
- 結構體成員 x(紅色)、y(藍色)、z(綠色)循序排列。
- 結構體成員之間並不緊密排列。
- 成員 y 的大小爲 4B,所以對齊係數爲 4,即便 x、z 的大小爲 1B,但仍然嚴格按照對齊係數進行佈局。
例 2:
#include<stdio.h>
struct {
int i;
char c1;
char c2;
} Test1;
struct {
char c1;
int i;
char c2;
} Test2;
struct {
char c1;
char c2;
int i;
} Test3;
int main()
{
printf("%d\n", sizeof(Test1)); // 輸出 8
printf("%d\n", sizeof(Test2)); // 輸出 12
printf("%d\n", sizeof(Test3)); // 輸出 8
return 0;
}
3 個結構體變量的內存佈局:
可見,從存儲結構體的空間首地址開始,每個成員被放置到內存中時,都會認爲內存是按照自己的大小來進行劃分的,因此成員的起始地址一定會在自身寬度的整數倍上,這就是所謂的內存對齊。
對於結構體而言,結構體成員的起始地址必須能夠被成員中所佔空間值最大的那個整除,例如:上述例子中的 int y。所以,通常結構體中成員變量聲明的順序是按照成員類型大小 從小到大 的順序進行,有時候這樣可以減少中間的填充空間。
下圖爲不準守 從小到大 順序原則的結果:
爲什麼要內存對齊?
內存對齊作爲一種強制的要求,有幾點原因:
-
簡化處理器與內存之間傳輸系統的設計。
-
平臺移植性:不是所有的硬件平臺都能訪問任意地址上的任意數據;某些硬件平臺只能在某些特定地址處取某些特定的數據,否則就會拋出硬件異常。也就是說在計算機在內存讀取數據時,只能在規定的地址處讀數據,而不是內存中任意地址都是可以讀取的。
-
提升讀取數據的速度:數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問,而對齊的內存訪問僅需要一次訪問。
假如沒有內存對齊機制,數據可以任意存放,現在一個 int 變量存放在從地址 1 開始的連續四個字節地址中,該處理器去取數據時,要先從 0 地址開始讀取第一個四字節塊,剔除不想要的字節(0 地址),然後從地址四開始讀取下一個四字節塊,同樣剔除不要的數據(5,6,7 地址),最後留下的兩塊數據合併放入寄存器。這需要做很多工作。
有了內存對齊後,int 類型數據只能存放在按照對齊規則的內存中,比如說 0 地址開始的內存。那麼現在該處理器在取數據時一次性就能將數據讀出來了,而且不需要做額外的操作,提高了效率。
對於 32 位系統,如下圖的 A 可能需要 2 條指令訪問,而 B 只需 1 條指令。
再舉 3 個例子:
- 成員變量對齊使用
int a[] = {'abcd', 4444};
typedef struct _GPIO_t {
char in;
char out;
char type;
char value;
int data;
} GPIO_t;
GPIO_t *GPIOA = (GPIO_t *)&a;
printf("%c \n", GPIOA->in);
printf("%c \n", GPIOA->out);
printf("%c \n", GPIOA->type);
printf("%c \n", GPIOA->value);
printf("%d \n", GPIOA->data);
注:數據存儲格式分爲大小端存儲,所以結構引用輸出順序可能不太對應。
- 成員變量沒有對齊使用
int a[] = {1234, 5678, 'abcd', 4444};
typedef struct _GPIO_t {
int in;
int out;
char type;
char value;
int data; // 會自動四字節對齊因此直接指向 a[3]
} GPIO_t;
GPIO_t *GPIOA = (GPIO_t *)&a;
printf("%d \n", GPIOA->in);
printf("%d \n", GPIOA->out);
printf("%c \n", GPIOA->type);
printf("%c \n", GPIOA->value);
printf("%d \n", GPIOA->data);
注:因爲自動對齊緣故,其中有些數據會自動丟掉。
- 成員變量不使用自動給對齊
int a[] = {1234, 5678, 'abcd', 4444};
#pragma pack(1) // 強制設置 1 字節對齊
typedef struct _GPIO_t {
int in;
int out;
char type;
char value;
int data; // 會自動四字節對齊因此直接指向 a[3]
} GPIO_t;
GPIO_t *GPIOA = (GPIO_t *)&a;
printf("%d \n", GPIOA->in);
printf("%d \n", GPIOA->out);
printf("%c \n", GPIOA->type);
printf("%c \n", GPIOA->value);
printf("%d \n", GPIOA->data);
注:最後一個成員由於對齊錯誤出現亂碼。
內存對齊跟平臺有關
需要注意的是,C 語言基本類型的大小是與平臺相關的。所以我們在進行內存對齊設計時,首先應該瞭解當前平臺的情況。
#include<stdio.h>
#define BASE_TYPE_SIZE(t) printf("%12s : %2d Byte%s\n", #t, sizeof(t), (sizeof(t))>1?"s":"")
void base_type_size(void)
{
BASE_TYPE_SIZE(void);
BASE_TYPE_SIZE(char);
BASE_TYPE_SIZE(short);
BASE_TYPE_SIZE(int);
BASE_TYPE_SIZE(long);
BASE_TYPE_SIZE(long long);
BASE_TYPE_SIZE(float);
BASE_TYPE_SIZE(double);
BASE_TYPE_SIZE(long double);
BASE_TYPE_SIZE(void*);
BASE_TYPE_SIZE(char*);
BASE_TYPE_SIZE(int*);
typedef struct {
} StructNull;
BASE_TYPE_SIZE(StructNull);
BASE_TYPE_SIZE(StructNull*);
}
int main()
{
base_type_size();
return 0;
}
執行結果:
void : 1 Byte
char : 1 Byte
short : 2 Bytes
int : 4 Bytes
long : 8 Bytes
long long : 8 Bytes
float : 4 Bytes
double : 8 Bytes
long double : 16 Bytes
void* : 8 Bytes
char* : 8 Bytes
int* : 8 Bytes
StructNull : 0 Byte
StructNull* : 8 Bytes
對齊係數
每個特定平臺上的編譯器都有自己默認的對齊係數。
使用 pragma 宏指令修改對齊係數
對齊係數是可以改變的,通過預處理器指令 #pragma pack(n),n=1,2,4,8,16
來自定義對齊係數,通過 #pragma pack()
,來取消自定義對齊係數。
#pragma pack(1)
typedef struct {
char e_char;
long double e_ld;
} S14;
#pragma pack()
宏定義 pragma pack(value) 的 value 就是指定的對齊值,通常 value 的值取 2 的較小次方。
- 如果 value 的值小於變量類型的對齊值,則按照 value 的值進行對齊。
- 如果 value 的值大於變量類型的對齊值,則按照原來的對齊值進行對齊。
簡而言之,使用該宏的時候,按照 value 值和原來對齊值之間較小的值進行對齊。
對於上例子的三個結構體,如果前面加上 #pragma pack(1)
,那麼此時有效對齊值爲1字節,此時根據對齊規則,不難看出成員是連續存放的,三個結構體的大小都是 6 字節。
如果前面加上 #pragma pack(2)
,有效對齊值爲 2 字節,此時根據對齊規則,三個結構體的大小應爲 6,8,6。內存分佈圖如下:
另外,還有如下的一種修改方式:
__attribute((aligned (n)))
,讓所作用的結構成員對齊在 n 字節自然邊界上。如果結構中有成員的長度大於 n,則按照最大成員的長度來對齊。attribute((packed))
,取消結構在編譯過程中的優化對齊,按照實際佔用字節數進行對齊。
內存對齊的原則
-
結構體成員按自身數據類型的對齊係數進行對齊:第一個結構體成員放在 offset(偏移量)爲 0 的地方,後續每個成員的起始地址要從該成員大小的整數倍開始。
-
結構體中成員爲其他結構體,則結構體成員要按自身結構體內部的最大對齊值(成員中的數據類型所佔空間值最大的那個)進行對齊:比如 struct A 中包含 struct B 類型的成員,B 中有 char、int、double 元素,那麼 B 應該從 sizeof(double) 的整數倍開始存儲。
-
結構體的自身對齊值是其成員中自身對齊值最大的那個值:即:結構體的總大小必須是其內部最大成員的整數倍,不足的要補齊。
下面通過幾個例子,加深對這幾個原則的理解(32 位系統):
EXAMLE1: sizeof(A)
= 8 字節,因爲兩個成員變量 int 都是 4 字節;sizeof(B)=8 字節,因爲原則 1.,如果將 b1 和 b2 的位置互換,則準守原則 3.。
struct A {
int a1; // 4B
int a2; // 4B
};
struct B {
char b1; // 1B
int b2; // 4B
};
EXAMLE2:sizeof(C)
= 8 字節,而擁有相同成員定義的 D 則爲 sizeof(D)=12 字節。C 和 D 的區別在於兩者定義的成員的順序不一樣,這導致了兩者佔用內存的大小也不一樣。因爲 C 按原則 1. 進行對齊後,剛好滿足原則 3.;而 D 中將 char 放在最後,按原則 1. 進行對齊後,並不滿足原則 3.,還需要按照原則 3. 進行補齊,所以佔用了額外的空間。
struct C {
short c1; // 2B
char c2; // 1B
long c3; // 4B
};
struct D {
short d1; // 2B
long d2; // 4B
char d3; // 1B
};
EXAMPLE3:sizeof(E)
= 16 字節,因爲原則 2. 要求成員結構體 e2,需要按照 C 的對齊值 4 對齊,所以內存從 offset(偏移值)爲 4 的地方開始存儲。將 e2 和 e3 互換位置,可以得到 sizeof(E)=12 字節。
struct E {
char e1; // 1B
struct C e2; // 8B
short e3; // 2B