C++ 编程优化小结
帕累托法则提示我们高效的编码要聚焦在会产生性能瓶颈的20%的地方,而非明显产生性能瓶颈的位置,需要优先考虑可维护性。
1 数据结构设计
- 优先采用四字节对齐
- 连续访问的数据连续存放,提高 D-Cache 命中率
- 减少内存块的数量,避免内存浪费。由于内存有最小分配粒度(如 4KB 对齐),且会附加一些元数据(内存头/尾)跟踪内存块大小、使用情况等,实际分配的内存会大于申请内存
- 避免大数据结构在全局赋值,由于会占用数据段,伴随整个程序的生命周期,不可取。
2 高效内存管理
动态内存,指在函数调用过程中频繁释放堆上内存,触发堆上内存管理器的内存分配方式。
它是最影响效率的因素之一,短时间内频繁的小内存申请释放,导致产生大量的内存碎片,无法及时回收利用,可能产生内存空洞,从而导致申请不到内存。
因此在一些高性能的场合,要避免产生动态内存,如:周期任务、配置恢复等。
2.1 STL 容器
避免在高频函数中,临时使用在堆上分配的 STL 容器,如 vector、map、list、string 等,建议使用 std::array 结构。
2.2 移动语义
C++11后开始支持移动语义,减少了内存拷贝。
注:
① 优先使用栈内存,栈相比堆内存更高效
② 频繁使用的对象,建议用内存池化
③ 可以使用系统提供的其他更高效的分配器
3 容器与算法选择
选择时应该从下面几个方向进行考虑:
- 节点内存是连续还是离散,会影响到内存占用,Cache利用率
- 关注增删改,或更关注查询
- 算法复杂度
注:
使用 vector 时,由于扩容将带来性能损失,因此通常创建时进行其大小分配。
4 语言特性提升
-
初始化列表
A(uint32_t v1, double v2):v1_(v1), v2_(v2) {}使用初始化列表,类中的基本成员类型会在分配内存时被初始化;避免调用不必要的默认构造,并省略了赋值操作,效率得到提升。
-
标识符使用
可以使用
constexpr将计算从运行时提前到编译期间,这能提升程序在运行时的效率,相反的会增加编译时期的时间。 -
循环语句
uint32_t table[10][10] = {}; for (int i = 0; i < 10; ++i) { for (int j = 0; j < 10; ++j) { uint32_t tmp = table[i][j]; } }当使用循环语句来访问数据时,建议先行后列的方式访问,这样能充分利用 CPU Cache
-
避免使用 RTTI 机制
RTTI(Runtime Type Identification)机制,能通过 typeid、type_info 和 dynamic_cast 等运算符,实现运行阶段的对象类型识别。
但它带来了额外的内存与运行效率开销,性能敏感的流程会代码明显的劣化。
5 并发与锁
5.1 伪共享
该问题是多个线程访问不同数据,但数据恰好处于同一个缓存行(Cache Line)中,它们的之间的缓存会不断的进行同步(无论数据是否发生变化),这回产生不必要的开销。
我们能通过填充数据结构(padding)来保证每个线程使用独立的缓存行。
顺带讲解 RFO(Read For Ownership)的机制,例如,线程 A 读取某共享变量的值,但发现该值已经被其他线程修改或读取,此时处理器会发起 RFO 请求获取该数据的最新版本,这里将根据缓存一致性协议(MESI协议)来管理数据的读写权限。
若频繁发生 RFO 操作会导致缓存行失效,从而影响性能,因为每次访问都需要检查数据的最新版本并获取所有权。
6 编码常用模式
-
预先计算
在热点代码前先完成复杂的运算(如浮点数运算);或可提前到编译器,如利用 constexpr,static,assert 等关键字
-
延迟计算
将计算延迟到真正需要利用计算数据的地方,如:多分支操作时,任务放在真正需要的分支中执行,而不是放在分支选择之前
-
批量计算
收集多个 task 后一起处理,可以移除重复计算
-
缓存
保存或复用昂贵计算结果,来减少计算量。通常只有数据量小,但数据计算昂贵,且频繁访问时,才考虑引入缓存
-
剪枝
先处理小开销的检查判断,必要时才处理耗时的流程
-
特性隔离
一套代码支持多个产品并行开发,则若未将特性进行隔离,则可能增加大量不需要的流程和计算,而导致性能劣化
7 编译优化
-
合理使用 inline
函数调用过程中存在入栈\出栈等开销(寄存器保存,call等),且调用函数距离当前函数较远,可能未被加载到 I-Cache 中,从而存在指令加载的开销。
因此函数体较小(通常 < 10 行),且频繁调用,调用点较少的函数,建议定义成 inline。它能够将函数在编译时直接替换为函数体,让编译器根据上下文进行优化,且能避免函数调用,I-Cache miss 带来的开销。
过渡使用 inline 容易造成二进制膨胀,可能导致执行效率下降;且它会造成维护调试上的一些困难,如 inline 后无法热补丁等
-
返回值优化
C++标准规定:满足返回值优化条件时,即使编译器不做返回值优化,也要把返回表达式做右值处理,优先进行移动而不是拷贝。
故返回表达式的类型,保证和函数的返回类型一致。
-
谨慎使用 volatile