【C/C++】Big Endian 和 Little Endian記憶體對齊
Big Endian 和 Little Endian記憶體對齊
由於目前的工作需要,所以學習了一下計算機記憶體對齊的相關知識,先介紹計算機的儲存方式:Big Endian與Little Endian:
- Big Endian 即資料的高位在低地址,地位在高地址,並且把最高位元組的地址作為變數的首地址
- Little Endian 即資料的高位在高地址,資料的低位在低地址,並且把最低位元組的地址作為變數首地址。
現實中,某些基於RISC(精簡指令集)的cpu比如SPARC、PowerPC等,採用Big Endian,而Intel系列cpu採用Little Endian。如果想要知道自己的電腦是什麼儲存格式只需要輸入以下程式碼:
#include<iostream>
using namespace std;
void main()
{
char ch[]={0x12,0x34,0x56,0x78};
int* p=(int*)ch;
cout<<hex<<*p<<endl;//如果是78563412,說明是 Little Endian,如果是12345678,則是Big Endian
}
自然對齊:如果一個變數的記憶體地址正好位於它位元組長度的整數倍,它就被稱做自然對齊
對於標準資料型別,它的地址只要是它的長度的整數倍,而非標準資料型別按下面的原則對齊:
陣列 :按照基本資料型別對齊,只要第一個對齊後面的自然也就對齊。
聯合 :按其包含的長度最大的資料型別對齊。
結構體: 結構體中每個資料型別都要對齊。
位元組對齊的好處:
位元組對齊的根本原因在於CPU訪問資料的效率問題。學過微機原理的都知道規則字和非規則字,8086cpu訪問規則字只要一個週期,而訪問非規則字需要兩個週期。在這裡原理也是一樣的,只不過這裡是32位的作業系統,最多一次訪問4位元組,而8086是16位的,一次最多訪問2位元組。假設上面整型變數的地址是自然對齊,比如為0x00000000,則CPU如果取它的值的話需要訪問一次記憶體,一次直接取從0x00000000-0x00000003的一個int型,如果變數在0x00000001,則第一次訪問0x00000001的char型,第二次取從0x00000002-0x00000003的short型,第三次是0x00000004的char型,然後組合得到所要的資料,如果變數在0x00000002地址上的話則要訪問兩次記憶體,第一次為short,第二次為short,然後組合得到整型資料。如果變數在0x00000003地址上的話,則與在 0x00000001類似。
我們通過下面的例子來說明自然對齊:
#include<iostream>
using namespace std;
void main()
{
int a=0x0abcde11;//a b c 的地址依次減小
int b=0x012345678;
double c=0x0f23456789abcdef1;
char d=0x0fa;
char *ptr=(char*)&a;
printf("a b每個位元組的內容:\n");
printf(" 地址 :內容\n");
for(int i=0;i<8;i++)
printf("%x :%x\n",ptr+3-i,*(ptr+3-i));//說明整數是按 little-endian儲存的
printf("\na b c d的首地址和地址與位元組長度的餘值:\n");
printf("a: %x :%d\n",&a,long(&a)%sizeof(a));//從這裡可以看成變數的記憶體地址按變數順序遞減的
printf("b: %x :%d\n",&b,long(&b)%sizeof(b));//各個變數並不一定存放在連續的記憶體單元
printf("c: %x :%d\n",&c,long(&c)%sizeof(c));
printf("d: %x :%d\n",&d,long(&d)%sizeof(d));
}
執行結果
由上面的結果可以知道:
- 地址隨變數順序而減小(你可以通過改變變數定義順序來測試);
- 我的電腦採用的是Little Endian;
- 各個變數並不一定存放在連續的記憶體單元(由c d的地址可知)
對於陣列,無論是靜態陣列還是動態陣列都是連續儲存的,可以用下面程式來檢視:
#include<iostream>
using namespace std;
void main()
{
int array[5]={0};
for(int i=0;i<5;i++)
cout<<&array[i]<<endl;//輸出靜態陣列的每個元素的地址
cout<<endl;
int *pt=new int[5];
for( i=0;i<5;i++)
cout<<hex<<(pt+i)<<endl;//輸出動態陣列的每個元素的地址
cout<<endl;
delete []pt;//注意要釋放記憶體
}
上面我們討論了基本資料型別的記憶體儲存,下面我們來看看類的儲存結構:
首先我們看看下面這個類:
class person1
{
bool m_isMan;
float m_height;
bool m_isFat;
double m_weight;
unsigned char m_books;
};
cout<<sizeof(person1)<<endl;//32=4+4+8+8+8
這裡person類的長度為32,其記憶體單元示意圖如下:
在這裡是按8位元組邊界來對齊的
上述變數已經都自然對齊了,為什麼person物件最後還要填充7位元組?
因為當你定義person型別的陣列時,如果不填充7位元組,則除了第一個元素外其它的元素就可能不是自然對齊了。
下面通過使用編譯指令來定義對齊方式:
#pragma pack(push,4)// 按4位元組邊界對齊
class person2
{
bool m_isMan;
float m_height;
bool m_isFat;
double m_weight;
unsigned char m_books;
};
cout<<sizeof(person2)<<endl;//24=4+4+4+8+4
#pragma pack(pop)
這裡person類的長度為24,其記憶體單元示意圖如下:
顯然,在這裡m_weight的地址不一定能被8整除,即不一定是自然對齊的。
從上面可以知道,記憶體的大小和存取的效率隨編譯方式和變數定義有關,最好的方法是:按照位元組大小從大到小依次定義變數成員,並儘可能採用小的成員對齊方式。
-從小到大定義變數:
//按照從小到大位元組長度來定義變數
class person4
{
bool m_isMan;
bool m_isFat;
unsigned char m_books;
float m_height;
double m_weight;
};
cout<<sizeof(person4)<<endl;//16=1+1+1+1位元組的填充+4+8
這裡person類的長度為16,其記憶體單元示意圖如下:
-從大到小定義變數:
//按照從大到小位元組長度來定義變數
class person3
{
double m_weight;
float m_height;
unsigned char m_books;
bool m_isMan;
bool m_isFat;
};
cout<<sizeof(person3)<<endl;//16=8+4+1+1+1+1位元組的填充
這裡person類的長度為16,其記憶體單元示意圖如下:
從上面可以看出兩者所佔記憶體一樣,但是穩定度不同,從小到大的方式的對齊方式而發生有的成員變數不會自然對齊。如下所示
#pragma pack(push,1)// 按4位元組邊界對齊
class person5
{
bool m_isMan;
bool m_isFat;
unsigned char m_books;
float m_height;
double m_weight;
};
cout<<sizeof(person5)<<endl;//15=1+1+1+4+8
#pragma pack(pop)
這裡person類的長度為15,其記憶體單元示意圖如下:
在上面的程式中,double的偏移量為1+1+1+4=7,很有可能不會自然對齊,所以最好採用從大到小的方式來定義成員變數。