從硬件到語言,詳解C++的內存對齊(memory alignment)


轉載請保留以下聲明
  作者: 趙宗晟
  出處: https://www.cnblogs.com/zhao-zongsheng/p/9099603.html

很多寫C/C++的人都知道“內存對齊”的概念以及規則,但不一定對他有很深入的了解。這篇文章試着從硬件到C++語言、更徹底地講一下C++的內存對齊。

什么是內存對齊(memory alignment)

首先,什么是內存對齊(memory alignment)?這個是從硬件層面出現的概念。大家都知道,可執行程序是由一系列CPU指令構成的。CPU指令中有一些指令是需要訪問內存的。最常見的就是“從內存讀到寄存器”,以及“從寄存器寫到內存”。在老的架構中(包括x86),也有一些運算的指令是可以直接以內存為操作數,那么這些指令也隱含了內存的讀取。在很多CPU架構下,這些指令都要求操作的內存地址(更准確的說,操作內存的起始地址)能夠被操作的內存大小整除,滿足這個要求的內存訪問叫做訪問對齊的內存(aligned memory access),否則就是訪問未對齊的內存(unaligned memory access)。舉例來說,ARM的LDRH指令從內存中讀取2個byte到寄存器中。如果指定的內存的地址是0x2587c20,因為0x2587c20這個數能夠被2整除,所以這2個byte是對齊的。而如果指定的內存的地址是0x2587c33,因為不能被2整除,所以是未對齊的。

那如果訪問未對齊的內存會出現什么結果呢?這個要看CPU。

  • 有些CPU架構可以訪問未對齊的內存,但是會有性能上的影響。典型的就是x86架構CPU
  • 有些CPU會拋出異常
  • 還有些CPU不會拋出任何異常,會靜默地訪問錯誤的地址
  • 近幾年也有些CPU的一部分指令可以正常訪問未對齊的內存,同時不會有性能影響

因為每個CPU對未對齊內存的訪問的處理方式都不一樣,所以訪問未對齊的內存是要盡量避免的。所以就出現了C/C++的內存對齊機制。

C++的內存對齊機制

在C++中每個類型都有兩個屬性,一個是大小(size),還有一個就是對齊要求(alignment requirement),或稱之為對齊量(alignment)。C++標准並沒有規定每個類型的對齊量,但是一般都會有這樣的規律。

  1. 所有基礎類型的對齊量等於這個類型的大小。
  2. struct, class, union類型的對齊量等於他的非靜態成員變量中最大的對齊量。

另外,標准規定所有的對齊量必須是2的冪。

編譯器在給一個變量分配內存時,都要算出並滿足這個類型的對齊要求。struct和class類型的非靜態成員變量的字節數偏移(offset)也要滿足各自類型的對齊要求。

舉例來說,

class MyObject
{
    char c;
    int i;
    short s;
};

c是char類型,對齊要求是1,i是int類型,對齊要求是4,s是short類型,對齊要求是2。那么MyObject取最大的,也就是4作為他的對齊要求。如果在某個函數中聲明了MyObject類型的變量,那么分配給這個變量的內存的起始地址是能夠被4整除的。

我們再看MyObject的成員變量。c是MyObject的第一個成員變量,所以他的字節數偏移是0,也就是說變量c占據MyObject的第一個byte。i的對齊要求是4,所以字節數偏移必須是4的倍數,又因為變量i必須在變量c的后面,於是i的字節數偏移就是4,也就是說變量i占據MyObject的第5到第8個byte,而第2到第4個byte則是空白填充(padding)。s的對齊要求是2,又因為s必須在i的后面,所以s的字節數偏移是8,也就是說,變量s占據MyObject的第9個和第10個byte。另外,因為struct、class、union類型的數組的每個元素都要內存對齊,所以一般來說struct、class、union的大小都是這個類型的對齊量的整數倍,所以MyObject的大小是12,也就是說,變量s后面會有2個byte的空白填充。

因為C++中所有內存訪問都是通過變量的讀寫來訪問的,這個機制確保了所有變量都滿足了內存對齊,也就確保了程序中所有內存訪問都是對齊的。

當然,C++不會阻止我們去訪問未對齊的內存。例如,以下的代碼就很可能會訪問未對齊的內存:

char buf[10];
int* ptr = (int*)(buf + 1);
++*ptr;

這類代碼是我們在實際工作中也是能遇到的。事實上這種寫法是比較危險的,因為他很可能會去訪問未對齊的內存。這也是為什么寫c++大家都不推薦用c風格的類型轉換寫法,而是要用static_cast, dynamic_cast, const_cast與reinterpret_cast。這樣的話,上面的代碼就必須要使用reinterpret_cast,大家都知道reinterpret_cast是很危險的,也許就會想辦法避免這樣的邏輯。

常見CPU的未對齊內存訪問

根據Intel最新的Intel 64及IA-32架構說明書,Intel 64及IA-32架構都支持未對齊內存的訪問,但是會有性能上的額外開銷(詳見http://www.intel.com/products/processor/manuals)。但是實際上最近的Core系列CPU已經可以無額外開銷訪問未對齊的內存。

而手機上最常見的ARMv8架構,如果是普通的、不做多核同步的未對齊的內存訪問,那么CPU可能會產生對齊錯誤(alignment fault)或者執行未對齊內存操作。換句話說,到底會報錯還是正常執行,是要看具體CPU的實現的。即使是執行正常操作,也會有一些限制。例如,不能保證讀寫的原子性(操作一個byte的除外),很可能產生額外的開銷等(詳見https://developer.arm.com/docs/ddi0487/latest/arm-architecture-reference-manual-armv8-for-armv8-a-architecture-profile)。ARMv8中的Cortex-A系列是手機上常見的CPU家族,他們就可以正常處理未對齊內存訪問,但是一般會有額外的開銷(詳見http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html)。

我們也可以寫一個簡單的程序測試一下自己的CPU對未對齊內存訪問的支持,以下是代碼:

#include <iostream>
#include <chrono>

using namespace std;
using namespace std::chrono;

milliseconds test_duration(volatile int * ptr)  // 使用volatile指針防止編譯器的優化
{
    auto start = steady_clock::now();
    for (unsigned i = 0; i < 100'000'000; ++i)
    {
        ++(*ptr);
    }
    auto end = steady_clock::now();
    return duration_cast<milliseconds>(end - start);
}

int main()
{
    int raw[2] = {0, 0};
    {
        int* ptr = raw;
        cout << "address of aligned pointer: " << (void*)ptr << endl;
        cout << "aligned access: " << test_duration(ptr).count() << "ms" << endl;
        *ptr = 0;
    }
    {
        int* ptr = (int*)(((char*)raw) + 1);
        cout << "address of unaligned pointer: " << (void*)ptr << endl;
        cout << "unaligned access: " << test_duration(ptr).count() << "ms" << endl;
        *ptr = 0;
    }
    cin.get();
    return 0;
}

我測試使用的電腦的CPU是Intel Core i7 2630QM,是intel 2代酷睿CPU,測試結果為:

address of aligned pointer: 000000668DEFFA78
aligned access: 282ms
address of unaligned pointer: 000000668DEFFA79
unaligned access: 285ms

可以看出對齊與未對齊的內存訪問沒有性能上的差別。

在C++中修改對齊要求

一般情況下,我們不需要自定義對齊要求,但也會有很特殊的情況下需要做調整。C++中,我們可以使用alignas關鍵字修改一個類型、或者一個變量的對齊要求。例如:

class MyObject
{
    char c;
    alignas(8) int i;
    short s;
};

這樣的話,變量i的對齊要求由原本的4變成了8,結果就是,i的字節數偏移由4變成了8,s的字節數偏移由8變成了12,MyObject的對齊要求也變成了8,大小變成了16。

我們也可以對MyObject的定義使用alignas:

class alignas(16) MyObject
{
    char c;
    int i;
    short s;
};

還可以在alignas里面寫某個類型。也可以使用多個alignas,結果就是使用最大的對齊要求。例如以下MyObject的對齊要求就是16:

class alignas(int) alignas(16) MyObject
{
    char c;
    int i;
    short s;
};

alignas有一個限制,那就是不能用alignas改小對齊要求。例如以下的代碼會報錯:

alignas(1) int i;

另外,C++中,有一個特殊的類型:max_align_t,所有不大於他的對齊量叫做基礎對齊量(fundamental alignment),比這個對齊量大的叫做擴展對齊量(extended alignment )。C++標准規定,所有平台必須要支持基礎對齊量,而對於擴展對齊量的支持要看各個平台。一般來說max_align_t的對齊量等於long double的對齊量。

C++關於內存對齊的支持還有很多功能,例如查詢對齊量的alignof關鍵字,可以創建任意大小任意對齊要求的類型的aligned_storage模板,還有方便模板編程的alignment_of等等,在此就不細述了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM