cpp优化

Optimizing software in C++:An optimization guide for Windows, Linux, and Mac platforms

不同C++ 结构的效率

不同变量类型的存储

栈存储

函数在调用过程中在栈上分配内存空间,返回时候再释放。函数调用过程中使用了相近范围的地址,如果没有很大的数组,这一部分的数据基本能缓存在一级cache中。

因此,变量和对象最好在使用他们的函数中声明

全局或静态存储

全局变量存储在静态内存中。

静态内存还用于static声明的变量,浮点常量(如3.5,整数常量一般包含在指令代码中了),字符串常量,switch跳转表,虚函数表。

静态数据优点是可以在程序启动前就完成初始化,缺点是内存空间在整个程序执行过程中都占用。

通常查找表最好声明为static const,比如

1
2
3
4
5
// Example 7.16
float SomeFunction (int x) {
static const float list[] = {1.1, 0.3, -2.0, 4.4, 2.5};
return list[x];
}

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版本会打开以下选项

  1. Use google tcmalloc代替标准的malloc(开关控制)
  2. 打开编译警告选项,

-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之类的开关
一般情况下,因为编译机器和生产机器有差异,一般情况下未打开

参考资料

容器类优化:https://www.agner.org/optimize/cppexamples.zip

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×