8.1 对齐支持

类别:部分人

8.1.1 数据对齐

在了解为什么数据需要对齐之前,我们可以回顾一下打印结构体的大小这个C/C++中的经典案例。首先来看看代码清单8-1所示的这个例子。

代码清单8-1


include <iostream>

using namespace std;

struct HowManyBytes{

char a;

int b;

};

int main(){

cout<<"sizeof(char):"<<sizeof(char)<<endl;

cout<<"sizeof(int):"<<sizeof(int)<<endl;

cout<<"sizeof(HowManyBytes):"<<sizeof(HowManyBytes)<<endl;

cout<<endl;

cout<<"offset of char a:"<<offsetof(HowManyBytes,a)<<endl;

cout<<"offset of int b:"<<offsetof(HowManyBytes,b)<<endl;

return 0;

}

//编译选项:g++ -std=c++11 8-1-1.cpp


在代码清单8-1中,结构体HowManyBytes由一个char类型成员a及一个int类型成员b组成。编译运行代码清单8-1所示的例子,我们在实验机平台上会得到如下结果:


sizeof(char):1

sizeof(int):4

sizeof(HowManyBytes):8

offset of char a:0

offset of int b:4


可以看到,在我们的实验机上,a和b两个数据的长度分别为1字节和4字节,不过当我们使用sizeof来计算HowManyBytes这个结构体所占用的内存空间时,看到其值为8字节。其中似乎多出来了3字节没有使用的空间。

出现这个现象主要是由于数据对齐要求导致的。通常情况下,C/C++结构体中的数据会有一定的对齐要求。在这个例子中,可以通过offsetof查看成员的偏移的方式来检验数据的对齐方式。这里,成员b的偏移是4字节,而成员a只占用了1字节内存空间,这意味着b并非紧邻着a排列。事实上,在我们的平台定义上,C/C++的int类型数据要求对齐到4字节,即要求int类型数据必须放在一个能够整除4的地址上;而char要求对齐到1字节。这就造成了成员a之后的3字节空间被空出,通常我们也称因为对齐而造成的内存留空为填充数据(padding data)。

在C++中,每个类型的数据除去长度等属性之外,都还有一项“被隐藏”属性,那就是对齐方式。对于每个内置或者自定义类型,都存在一个特定的对齐方式。对齐方式通常是一个整数,它表示的是一个类型的对象存放的内存地址应满足的条件。在这里,我们简单地将其称为对齐值。

对齐的数据在读写上会有性能上的优势。比如频繁使用的数据如果与处理器的高速缓存器大小对齐,有可能提高缓存性能。而数据不对齐可能造成一些不良的后果,比较严重的当属导致应用程序退出。典型的,如在有的平台上,硬件将无法读取不按字对齐的某些类型数据,这个时候硬件会抛出异常(如bus error)来终止程序。而更为普遍的,在一些平台上,不按照字对齐的数据会造成数据读取效率低下。因此,在程序设计时,保证数据对齐是保证正确有效读写数据的一个基本条件。

虽然从语言设计者的角度而言,将对齐方式掩盖起来会使得语言更具有亲和力。但通常由于底层硬件的设计或用途不同,以及编程语言本身在基本(内置)类型的定义上的不同,相同的类型定义在不同的平台上会有不同的长度,以及不同的对齐要求。虽然系统设计者常常会在应用程序二进制接口中(Application Binary Interface,ABI)详细规定在特定平台上数据长度、数据对齐方式的相关信息,但是这两者存在着平台差异性则是不争的事实。在C++语言中,我们可以通过sizeof查询数据长度,但C++语言却没有对对齐方式有关的查询或者设定进行标准化,而语言本身又允许自定义类型、模板等诸多特性。编译器无法完全找到正确的对齐方式,这会在使用时造成困难。让我们来看一下代码清单8-2所示的这个例子。

代码清单8-2


include <iostream>

using namespace std;

//自定义的ColorVector,拥有32字节的数据

struct ColorVector{

double r;

double g;

double b;

double a;

};

int main(){

//使用C++11中的alignof来查询ColorVector的对齐方式

cout<<"alignof(ColorVector):"<<alignof(ColorVector)<<endl;

return 1;

}

//编译选项:clang++-std=c++11 8-1-2.cpp


在代码清单8-2所示的例子中,我们使用了C++11标准定义的alignof函数来查看数据的对齐方式。编译运行代码清单8-2,我们可以看到ColorVector在实验机上依然是对齐到8字节的地址边界上。


alignof(ColorVector):8


这与我们设计ColorVector的原意是不同的。现在的计算机通常会支持许多向量指令,而ColorVector正好是4组8字节的浮点数数据,很有潜力改造为能直接操作的向量数据。这样一来,为了能够高效地读写ColorVector大小的数据,我们最好能将其对齐在32字节的地址边界上。

在代码清单8-3所示的例子中,我们利用C++11新提供的修饰符alignas来重新设定ColorVector的对齐方式。

代码清单8-3


include <iostream>

using namespace std;

//自定义的ColorVector,对齐到32字节的边界

struct alignas(32)ColorVector{

double r;

double g;

double b;

double a;

};

int main(){

//使用C++11中的alignof来查询ColorVector的对齐方式

cout<<"alignof(ColorVector):"<<alignof(ColorVector)<<endl;

return 1;

}

//编译选项:g++ -std=c++11 8-1-3.cpp


编译运行代码清单8-3所示的代码,我们会得到如下结果:


alignof(ColorVector):32


正如我们在代码清单8-3中所看到的,指定数据ColorVector对齐到32字节的地址边界上,只需要声明alignas(32)即可。接下来我们会详细讨论C++11对对齐的支持。