本篇介绍的内容都是提升程序运行效率相关的内容,但是使用不当有可能会对程序的稳定性造成影响或者反而使效率下降,因此使用前要有充分的了解和把握。
对齐支持alignof和alignas
我们都知道结构体数据会进行字节对齐以保证运行效率,再让我们回顾一下具体的原因。
一、什么是字节对齐
现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。
二、对齐的原因和作用
不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。
但最常见的情况是,如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。因此,通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。此外,合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。
在C++11之前对齐方式是无法得知的,只能自己判断,且不同的平台实现方式可能不同,在C++11中可以用alignof操作符计算出对应类型的对齐长度:
struct Example {
char c;
int i;
long long l;
};
int main()
{
std::cout << sizeof(Example) << std::endl; // 16
std::cout << alignof(Example) << std::endl; // 8
std::cout << alignof(int) << std::endl; // 4
return 0;
}
Example结构体会进行数据对齐,从以上的代码可以看出,Example占用16字节的内存,alignof(Example)的结果是8,也就是说对齐长度为8字节,long long 占用8字节,int和char一起占用8字节。
有时候可能也需要修改默认的对齐方式以提升效率,因此C++11也提供了对数据对齐方式进行设置的方法,alignas操作符能够改变对齐方式:
struct Color {
double r;
double g;
double b;
double a;
};
int main()
{
std::cout << alignof(Color) << std::endl; // 8
return 0;
}
上面的举例了用结构体Color来表示颜色,可以看到默认对齐方式为8字节。而实际上我们更希望对齐方式为32字节以支持计算机的向量指令,提高运行效率,此时只需要做一些修改即可:
struct alignas(alignof(double)*4) Color {
double r;
double g;
double b;
double a;
};
int main()
{
std::cout << alignof(Color) << std::endl; // 32
return 0;
}
通用属性
C++11预定义的通用属性包括[[ noreturn ]]以及[[ carries_dependency ]]。
[[ noreturn ]]主要用于标识哪些不会返回控制流的函数,典型的例子:有终止应用程序语句的函数、有无限循环的函数、有异常抛出的函数等。通过这个属性标识后编译器能更好的对这些函数进行告警或者优化。例如:
[[ noreturn ]] void func()
{
throw "error";
}
int main()
{
func();
return 0;
}
func函数声明了noreturn属性,那么编译器就不会为func函数之后生成代码,或者做出警告。但是当函数有可能返回控制流的时候,不要用noreturn 属性进行修饰,因为这会造成不可预知的后果。
[[ carries_dependency ]]主要用于消除内存顺序一致性,在《深入理解C++11》笔记-原子类型和原子操作中介绍过内存顺序。当指令的运行顺序不影响结果时,内存乱序能够提升处理器的处理效率,而乱序是会被函数破坏的:
int a = 0;
func();
a += 1;
例如以上的代码,由于编译器在编译到func时不知道func中的具体实现,往往会保守的把这段代码按照顺序执行,而[[ carries_dependency ]]属性就能消除这个问题,只需要在func函数声明用这个属性修饰:
[[ carries_dependency ]] func();
但是需要保证func函数的实现,的确不会因为运行顺序对运行结果造成影响。