Optimizing software in C++:An optimization guide for Windows, Linux, and Mac platforms
不同C++ 结构的效率
不同变量类型的存储
栈存储
函数在调用过程中在栈上分配内存空间,返回时候再释放。函数调用过程中使用了相近范围的地址,如果没有很大的数组,这一部分的数据基本能缓存在一级cache中。
因此,变量和对象最好在使用他们的函数中声明
全局或静态存储
全局变量存储在静态内存中。
静态内存还用于static声明的变量,浮点常量(如3.5,整数常量一般包含在指令代码中了),字符串常量,switch跳转表,虚函数表。
静态数据优点是可以在程序启动前就完成初始化,缺点是内存空间在整个程序执行过程中都占用。
通常查找表最好声明为static const,比如
1 | // Example 7.16 |
static可以让这个表第一次调用就初始化,后面就不需要了。这样多了一个开销是要检查这是第一次调用还是已经被调用,因此引入const让编译器确定这个表永远不会变化,不需要对第一次调用进行检查。
寄存器存储
编译器优化时会自动选择函数中常用的变量存储在寄存器。这也是推荐使用局部变量的一个原因(另一个是栈存储的cache命中高)
易变变量(volatile)
valatile指明变量可能被其他线程修改,阻止编译器进行用从先前分配的值进行优化。
线程本地存储(TLS)
关键字__thread
或__declspec(thread)
实现静态变量和全局动态变量线程本地存储。tls比较低效,通过存储在线程环境块中的指针进行访问。应该避免,使用栈上的存储,存储在栈上的变量综述属于创建他们的线程。
Far
在分段内存的系统中用
动态内存分配
当以随机分配释放大小不同的对象,堆容易碎片化。堆碎片化时,顺序分配的对象不一定顺序分配在其中,造成cache效率低下。
类中声明的变量
类中声明的变量按照声明的顺序存储。static修饰的类成员变量存储在静态内存中,只有一个实例。
整型变量和运算符
整数大小
建议使用stdint.h中的整数类型,如int8_t定义特定大小的整数类型,可移植性也强。
但是避免大于寄存器带下的整数,如32位系统中使用64位int。
size_t在32位系统中是32位,在64位系统中是64位。
注意检查整数溢出问题
有符号整数vs无符号整数
多数情况下,有符号整数和无符号整数在速度上没有区别,但在某些情况下:
除以常数和取模:无符号要快
大多数指令集,有符号整数转成浮点数要快
两者的溢出行为不同,无符号溢出得到小整数
有符号整数和无符号整数的转化无代价,负整数被阶数为大整数。
不要比较(如>)有符号整数和无符号整数。
整数运算符
- 简单整数操作,如加减、比较、位操作、移位只要一个时钟周期
- 乘法3-4个周期,除法40-80个时钟周期
自增和自减
++i和i++是一样快的。
一些下情况,如x = array[i++]比x = array[++i]快,因为计算数组元素地址不用等待i的新值。
a = ++b比a=b++快,以为编译器会认为a=b,这样就可以用相同的编译器
浮点变量和运算符
x86中有两种不同类型的浮点寄存器
枚举
enum是一个隐藏的整形,效率跟整数一样
bool
布尔操作数的顺序
如a&&b ,a||b
如果a和b的计算时间相同,分支预测的预测可能性相同,把true多的操作数放到&&最后,||最前
计算速度快的操作符放前
可预测的操作数放前
布尔变量被过度检查
bool变量放在8位int中,编译器会检查以bool变量作为运算符输入时是否为0和1,因此布尔变量作为输入可以优化。
如用char代替bool
布尔向量操作
ab都是32位整数,则y=a&b可以在一个时钟周期内完成
指针和引用
指针vs引用
两者效率一样,编译器实现一样,区别在于编程风格
使用指针的优点:
- 指针变量可以被清楚的看到,引用在函数体内跟变量的写法是一样的
- 指针的指向位置可以改变
使用引用的优点:
- 语法简单
- 更安全,引用一定指向一个有效的地址
- 拷贝构造函数和重载运算符中常用
- 被声明位const引用的函数参数接受表达式作为参数,而指针和非常量引用需要一个变量
效率
编译器中的优化
编译器如何优化
函数内联
编译器可以用被调函数主体替换函数调用,如果函数很小,或者只从较少地方调用这个函数,编译器则可能使用函数内联。
- 节约了函数调用返回传参的开销
- 代码连续,代码cache效率高
常量折叠和常数传播
只包含常量的表达式或子表达的计算结果被替换,常量也可以通过一系列计算进行传播
消除指针
指针指向的目标已知,指针或引用可能被消除
消除公共子表达式
相同子表达式出现多次,那么编译器可能只计算一次。
寄存器变量
编译器把常用的变量作为寄存器变量
典型的有:临时中间变量、循环计数器、函数参数、指针、引用、this指针、公共子表达式、归纳变量
有指针指向和引用的变量不能存储在寄存器中,为了利用寄存器变量的优化,应该避免使用对这种变量使用指针或引用。
活动范围分析
活动范围:指变量被使用的代码范围。
对活动范围不重叠的变量,编译器可以优化使用相同的寄存器
合并相同的分支
比如两个分支中用了相同的操作,编译器会把相同的操作提到分支外
消除跳转
通过复制跳转的代码来避免跳转
循环展开
循环展开并不一定就是最优的,因为展开太多代码会占用代码缓存空间
优化内存访问
cache结构
多路组相联形式,cache分为行和组
(addr/line size) % (set num)得到缓存组
内存中地址差值为关键步长的变量将争夺相同的缓存线
critical stride = (total cache size)/(ways num)
一起使用的函数放在一起
经常使用的函数和很少使用的函数分开,代码中关键部分中使用的函数集中放在一个源文件中。或者调整模块的连接顺序,调用多的链接在一起(针对静态链接)。
一起使用的变量放在一起
从缓存中加载一个变量只需要几个时钟周期,如果cache不命中,就要耗费超过100个时钟周期从ram中加载。
面向对象编程时将数据存储在一起的有效办法
在比如以a[0],b[0],a[1],b[1]...
访问数组,则可以用结构体数组组织
struct Sab{int a,int b}
然后 用Sab[i]
来访问
数据对齐
变量存储在可被变量大小整除的内存地址,这样访问效率最高
对象和数组对其与cache line大小一致,这样可以保证对象或数组的起始位置刚好位于cache line起始位置。
动态分配内存
可以用alloc代替new或者malloc
alloca的优势:
- 分配的开销小,cpu有对栈操作的硬件支持。
- 栈的先进后出的特性,不会造成内存碎片
- 不需要垃圾收集
- 跟栈上的其他数据对象连续,cache更高效
容器类
STL以通用性和灵活性为准测设计,而在执行速度、内存占用、缓存效率上考虑较低。
补救措施是使用内存池,参考容器类优化代码
字符串
string在每次创建和修改字符串时使用new来分配新的内存块,如果程序多次创建修改字符串,这样做是低效的。
编译优化
CMAKE配置的编译选项,有很多开关,基本上Release版本会打开以下选项
- Use google tcmalloc代替标准的malloc(开关控制)
- 打开编译警告选项,
-Wall
-Wextra
-Wlogical-op
-Wcast-align
-Wdisabled-optimization
-Wvector-operation-performance
-Wstack-protector
-Wno-ignored-qualifiers
-Wno-unused-parameter
3. 把编译警告当成错误(开关控制, -Werror)
4. Enable C++ 20
5. -O3 -DNDEBUG
6. LTO优化相关选项(开关控制)
-flto -fuse-linker-plugin -fdevirtualize-speculatively -Wmaybe-uninitialized
7. Fast math
-ffast-math
8. 其它
-pipe -Winvalid-pch -pthread
-Wl,–disable-new-dtags
9. native或AVX之类的开关
一般情况下,因为编译机器和生产机器有差异,一般情况下未打开