首页 > 编程笔记 > C++笔记 阅读:41

C++ alignas和alignof的用法(非常详细)

C++11 标准提供了用于指定或获取一种类型的对齐方式的方法,在此之前只能依赖编译器特有的办法。控制对齐方式对于提高不同处理器的性能并启用某些仅适用于特定对齐方式的数据的指令非常重要。

例如,对于 Intel SSE (Streaming SIMD Extensions)和 Intel SSE2 的处理器,如果按 16 字节对齐,它们处理相同的数据的速度会得到大幅提升。另外,对于 Intel AVX(Intel Advanced Vector Extensions),它将大多数整数处理器的命令扩展到 256 位,强烈建议使用 32 字节对齐。

本节探讨如何用 alignas 标识符来控制字节对齐方式。

C++ alignas和alignof的使用

控制数据类型或对象的对齐方式(既包含类层面的,也包含数据成员层面的),用 alignas 标识符:
struct alignas(4) foo
{
    char a;
    char b;
};
struct bar
{
    alignas(2) char a;
    alignas(8) int b;
};
alignas(8) int a;
alignas(256) long b[4];

获取数据类型的对齐方式,用 alignof 操作符:
auto align = alignof(foo);

C++ alignas和alignof的工作原理

处理器不会一次只访问一个字节,一般会访问一块比较大的区域,这块区域的大小一般为 2 的整数幂(2、4、8、16、32 等)。

基于此,为了提高处理器的处理速度,编译器对于内存的数据对齐就显得尤为重要。如果数据未对齐,编译器必须做额外的工作来访问这些未对齐的数据,例如,它必须读取成倍的数据块,然后裁剪并丢弃不需要的字节,最后将它们组合在一起。

C++ 编译器是根据数据类型来对齐变量的。该标准仅指定 char、signed char、unsigned char、char8_t 和 std::byte 的大小必须为 1。它还规定 short 的大小至少为 16 位,long 的大小至少为 32 位,long long 的大小至少为 64 位。同时也规定:

1==sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long)<=sizeof(long long)

因此,大多数类型大小是由编译器指定的且依赖于平台。比较典型的是,bool 和 char 类型的大小为 1 字节,short为 2 字节,int、long 和 float 为 4字 节,double 和l ong long 为 8 字节,等等。

当涉及结构或联合体时,对齐方式必须与最大成员的大小匹配,以避免出现性能问题。为了说明这一点,我们来探究一下下面的数据结构
struct foo1 // size = 1, alignment = 1
{           // foo1:    +-+
    char a; // members: |a|
};

struct foo2 // size = 2, alignment = 1
{           // foo2:   +-+-+
    char a; // members |a|b|
    char b;
};

struct foo3 // size = 8, alignment =4
{           // foo3:     +----+----+
    char a; // members: /a..../bbbb/
    int b;  // . represents a byte of padding
};
foo1 和 foo2 的大小不一样,但是对齐方式是一样的,(即都按 1 字节对齐),因为所有的成员都是 1 字节 char 类型的。

foo3 的第二个成员是整型,它的大小是 4 字节。因此,这个结构的成员对齐的地址是 4 的整数倍。为了达到这个要求,编译器会填充一些字节。

结构 foo3 实际上被转化成了下面这样:
struct foo3_
{
    char a_;      // 1 byte
    char _pad[3]; // 3 bytes padding to put b on a 4-byte boundary
    int b;        // 4 bytes
};

类似地,下面这个结构的大小是 32 字节,并且按 8 字节对齐,这是因为它有一个大小为 8 字节的 double 数据成员。因此,这个结构需要填充更多的字节,以确保访问的地址为 8 的倍数:
struct foo4 // size = 24, alignment =8
{           // foo4:   +--------+--------+--------+--------+
    int a; // members: |aaaa....|cccc....|dddddddd|e.......|
    char b; // . represents a byte of padding
    float c;
    double d;
    bool e;
};
编译器生成的等价结构如下:
struct foo4_
{
    int a_; //4 bytes
    char b; //1 byte
    char _pad0[3]; //3 bytes padding to put c on a8-byte boundary
    float c; //4 bytes
    char _pad1[4]; //4 bytes padding to put d on a8-byte boundary
    double d; //8 bytes
    bool e; //1 byte
    char _pad2[7]; //7 bytes padding to make sizeof struct multiple of 8
};

在 C++11 中,可以使用 alignas 标识符来指定对象或类型的对齐方式。这可以采用表达式(计算结果为 0 的整数常量表达式或对齐的有效值)、type-id 或者参数包来表示。alignas 标识符可应用于不表示位字段的变量或类数据成员的声明,也可应用于类、联合体或枚举结构的声明。

用 alignas 标识符声明的类型或变量的对齐字节数等于 alignas 表达式中的最大值(大于 0)。

使用 alignas 标识符有一些限制:
在下面的例子中,alignas 标识符被用于类声明。没有用 alignas 标识符声明的默认对齐字节是 1,但是当使用 alignas(4) 时,对齐字节数变成了 4:
struct alignas(4) foo
{
    char a;
    char b;
};
换句话说,编译器将前面的类翻译成了下面这个:
struct foo
{
    char a;
    char b;
    char _pad[2];
};

alignas 标识符既可以用于类声明,也可以用于成员数据声明。在这种情况下,采取最“严格”的原则来确定对齐的字节数。

在下面的例子中,成员 a 的默认大小为 1 字节,但是被要求按 2 字节对齐,成员 b 的默认大小为 4 字节,但是被要求按 8 字节对齐,所以最严格的对齐字节数是 8。整个类要求按 4 字节对齐,但是比最严格的 8 字节对齐要宽松,所以会被忽略,但是编译器会给出一个警告:
struct alignas(4) foo
{
    alignas(2) char a;
    alignas(8) int b;
};
上述结构等同于:
struct foo
{
    char a;
    char _pad[7];
    int b;
    char _pad1[4];
};

alignas 标识符也可以用来修饰变量。在下面的例子中,整数变量 a 被要求放在内存地址为 8 的整数倍的地方。变量 b(代表 4 个 long 类型元素的数组)被要求放在内存地址为 256 的整数倍的地方。因此,编译器会在两个变量之间填充 244 字节(取决于变量 a 在内存的位置,即内存地址为 8 的倍数的位置):
alignas(8) int a;
alignas(256) long b[4];

printf("%p\n", &a); // eg. 000000000D9EF908
printf("%p\n", &b); // eg. 000000000D9EFA00
看下输出的内存地址,我们可以看到变量 a 的内存地址确实是 8 的整数倍,且 b 的内存地址也确实是 256(16 进制表示是 0x100)的整数倍。

如果想查看类型的对齐字节数,可以使用 alignof 操作符。它不同于 sizeof,sizeof 操作符只适用于 type-id,却不适用于变量或者类成员。它使用的类型可以是是完全类型、数组类型或引用类型。对于数组而言,对齐字节数为每个元素的对齐字节数;对于引用而言,对齐字节数为其引用的对齐字节数。

下表给出了一些示例:

表达式 计算结果
alignof(char) 1,因为 char 是按 1 字节对齐的
alignof(int) 4,因为 int 是按 4 字节对齐的
alignof(int*) 指针对齐,在 32 位平台是 4,在 64 位平台是 8
alignof(int[4]) 4,因为数组的元素类型是 int,所以按 4 字节对齐
alignof(foo&) 8,因为前面的例子中指定的对齐字节是 8

如果想强制数据对齐(考虑到前面提到的限制)以便数据访问和复制比较高效,alignas 标识符则非常有用。这意味着 CPU 可以避免读写缓存行失效。这在性能关键型应用中显得尤为重要,比如游戏和交易应用。另外,alignof 操作符返回指定类型的最小对齐要求。

相关文章