不少写C/C++的人都知道“内存对齐”的概念以及规则,但不必定对他有很深刻的了解。这篇文章试着从硬件到C++语言、更完全地讲一下C++的内存对齐。html
首先,什么是内存对齐(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对未对齐内存的访问的处理方式都不同,因此访问未对齐的内存是要尽可能避免的。因此就出现了C/C++的内存对齐机制。编程
在C++中每一个类型都有两个属性,一个是大小(size),还有一个就是对齐要求(alignment requirement),或称之为对齐量(alignment)。C++标准并无规定每一个类型的对齐量,可是通常都会有这样的规律。数组
另外,标准规定全部的对齐量必须是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是很危险的,也许就会想办法避免这样的逻辑。
根据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++中,咱们可使用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等等,在此就不细述了。