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

轉載請保留如下聲明
  做者: 趙宗晟
  出處: http://www.javashuo.com/article/p-uwkhzifg-dr.html

不少寫C/C++的人都知道「內存對齊」的概念以及規則,但不必定對他有很深刻的瞭解。這篇文章試着從硬件到C++語言、更完全地講一下C++的內存對齊。html

什麼是內存對齊(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整除,因此是未對齊的。ios

那若是訪問未對齊的內存會出現什麼結果呢?這個要看CPU。c++

  • 有些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)也要知足各自類型的對齊要求。jsp

舉例來講,函數

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等等,在此就不細述了。

相關文章
相關標籤/搜索