1. 程式人生 > >【C/C++】Big Endian 和 Little Endian記憶體對齊

【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,很有可能不會自然對齊,所以最好採用從大到小的方式來定義成員變數。