【C++开发的极限性能优化】

本文介绍了C++开发中的性能优化关键点,包括使用profiling工具识别瓶颈、避免在性能热点函数中过度使用虚函数、循环展开提升并行计算、内存对齐提高缓存效率、以及在NUMA架构下考虑cache-aware和核绑定的技巧。

1、profiling工具

性能分析是性能调优的前提,能帮助发现当前程序的性能瓶颈,指导性能优化的方向。
cpu-profiling工具:valgrind、linux perf、brpc-view(CPU-profiler),可用于分析性能瓶颈、热点函数、热点开销、耗时分布等。
mem-profiling工具:brpc-view(heap-profiler),可用于分析常驻内存消耗、内存申请热点、内存泄漏等问题。

2、性能热点函数中慎用虚函数

虚函数调用会引入额外性能开销,原因如下:

  • 间接调用:虚函数是通过虚函数表(vtable)来实现的,调用时需要查虚函数表找到真正的函数地址,这是一个间接调用的过程。
  • 内存访问:vtable通常存储在内存中,访问vtable需要从内存中读取数据,这比直接从CPU缓存中读取数据要慢。
  • cache miss:普通函数指令通常在函数地址附近,容易命中cache。vtable和object在内存中分离存储,大概率导致cache miss,导致性能退化
  • 无法编译优化:因为虚函数在编译时无法确定具体调用哪个函数实现,只能在运行时动态决定,无法享受编译优化带来的性能提升。
    优化方法:
  • 使用函数指针替代虚函数
  • 使用模板编程替代虚函数
  • 对不被继承的子类用final关键字修饰(final修饰后,编译器可进行优化,线下demo测试耗时可优化约1.5%)

3、循环展开

循环展开是指通过增加每次循环迭代中处理的数据数量,来减少循环次数。
循环展开对提升性能的帮助:

  • 减少分支预测失败的次数
  • 增加循环体内指令并行的可能性(需要依赖指令间不存在数据相关)
#include <iostream>
#include <sys/time.h>
float loop_v0() {
    float ret = 1.0f;
    for (uint32_t i = 1; i < 10000001; ++i) {
	ret *= i;
    }
    return ret;
}
float loop_v1() {
    float ret = 1.0f;
    float ret1 = 1.0f;
    float ret2 = 1.0f;
    for (uint32_t i = 1; i < 10000001; i+=2) {
	ret1 *= i;
        ret2 *= i+1;
    }
    ret = ret1 * ret2;
    return ret;
}

uint32_t timediff(struct timeval& first, struct timeval& sec) {
    return (sec.tv_sec - first.tv_sec) * 1000000 + (sec.tv_usec - first.tv_usec);
}

int main() {
    struct timeval tv0;
    gettimeofday(&tv0, NULL);
    float ret0 = loop_v0();
    struct timeval tv1;
    gettimeofday(&tv1, NULL);
    float ret1 = loop_v1();
    struct timeval tv2;
    gettimeofday(&tv2, NULL);
    std::cout << "tm0=" << timediff(tv0, tv1) << ",ret0=" << ret0 << std::endl; // 打印ret0是为了避免O3编译优化下,编译器判定loop_v0函数为dead code而不执行
    std::cout << "tm1=" << timediff(tv1, tv2) << ",ret1=" << ret1 << std::endl;
    return 0;
}

上面的demo,即使开O3编译优化,循环展开版本相比非循环展开版本也能有约50%的性能优化:

4、内存对齐

尽管内存是以字节为单位的,但是cpu一般不是按字节块从内存取数据,而是按照双字节、四字节、八字节等为单位来存取内存,有些特殊指令甚至是按128位、256位来存取内存(例如avx128、avx256 load命令)。
如果未做内存对齐,cpu在取数据时可能需要做多次内存访问,且可能需要做额外的位移、合并操作。做内存对齐后只需要一次内存访问,访存效率得到提升。
使用#pragma pack (n),编译器将按照n个字节对齐
使用#pragma pack (),取消自定义字节对齐方式

5、cache aware

cpu访问各部件的延迟差别很大,下面是2009年Jeff Dean给的一组数字:
(最新数据见:https://colin-scott.github.io/personal_website/research/interactive_latency.html)

访问延迟信息

对于互联网公司线上常见的Haswell CPU架构,CPU访问cache、RAM所需的始终周期数约:
(详细数据可参考:https://www.7-cpu.com/cpu/Haswell.html)

L1L2L3RAM
Latency(cycles)4-511-1325-40107-226
Size32KB256KB2-3MB x nb cores-

Cache-aware指在程序设计时应该充分利用cache系统的特征,尽可能利用低延迟cache部件,以提升程序性能。常见方法有:

  • 保证数据局部性,尽可能让数据在空间和访问时间上连续,从而提升cache命中率
  • 程序中需要频繁使用的数据应尽可能load到三级cache中,例如需要查表的程序中可以考虑对表进行拆分或压缩,尽可能将表常驻高速缓存中
  • 使用prefetch指令,提前将数据加载到缓存。prefetch是异步操作,可以指定将一个cache line(一般是64字节)的数据加载到指定cache level。prefetch的时机对性能影响非常大,prefetch最佳时机跟程序行为紧密相关,建议通过perf工具来分析和调整。
    可以使用cachegrind来分析程序的cache miss情况,可以看到函数级的cache miss。

6、NUMA亲和性与绑核

了解底层系统的实现对程序性能优化至关重要。
在传统的统一内存访问架构(UMA,Uniform Memory Access)下,所有处理器共享同一块内存,导致内存带宽成为瓶颈。
在现代多处理器系统中,大多采用非统一内存访问架构(NUMA, Non-Uniform Memory Access),在NUMA架构下,多个CPU被封装在一起,这种封装被称为CPU Socket(插槽),每个CPU Socket拥有自己的本地内存。
CPU访问本地内存的速度比访问其他Socket的远端内存更快。
CPU访问

假如一个本来在CPU Socket 0,它需要的数据被load到CPU Socket 0的本地内存,后来任务被调度到CPU Socket 1中,那CPU需要通过互连技术(QPI或IF)跨Socket访问远端内存,这样会导致访问耗时增加。
在NUMA架构下,我们期望的访存行为是尽可能访问本地内存,避免访问远端内存。
通常通过将进程或者线程绑定到特定CPU核来达到这个目的,常见的绑定方法有:

  • 配置numa亲和性(通常由paas平台配置),系统会从多个维度(如socket(插槽)、die(硅片)、CCX(缓存控制器))将所有CPU核划分成多个numa node,paas平台在创建实例时将实例绑定在特定的numa node上。
  • OpenMP程序通过OMP_PLACES、OMP_PROC_BIND绑定
  • C/C++程序可通过sched_setaffinity(2)、pthread_getaffinity_np(3)绑定

目前线上很多服务已经开启numa亲和性配置了,这种情况下绑核仍然是有意义的:

  • 有些cpu开销大的服务,paas分配的numa node可能会跨socket
  • 绑核可以减少cpu核切换的开销(如系统调度、cpu上下文切换、寄存器刷新、缓存失效等)
    绑核的时候需要注意绑定方式,避免cpu过载。

附:AMD Zen3架构图
AMD Zen3架构图

C++性能优化 指南(强列推荐) chm版 Part I: Everything But the Code Chapter 1. Optimizing: What Is It All About? Performance Footprint Summary Chapter 2. Creating a New System System Requirements System Design Issues The Development Process Data Processing Methods Summary Chapter 3. Modifying an Existing System Identifying What to Modify Beginning Your Optimization Analyzing Target Areas Performing the Optimizations Summary Part II: Getting Our Hands Dirty Chapter 4. Tools and Languages Tools You Cannot Do Without Optimizing with Help from the Compiler The Language for the Job Summary Chapter 5. Measuring Time and Complexity The Marriage of Theory and Practice System Influences Summary Chapter 6. The Standard C/C++ Variables Variable Base Types Grouping Base Types Summary Chapter 7. Basic Programming Statements Selectors Loops Summary Chapter 8. Functions Invoking Functions Passing Data to Functions Early Returns Functions as Class Methods Summary Chapter 9. Efficient Memory Management Memory Fragmentation Memory Management Resizable Data Structures Summary Chapter 10. Blocks of Data Comparing Blocks of Data The Theory of Sorting Data Sorting Techniques Summary Chapter 11. Storage Structures Arrays Linked Lists Hash Tables Binary Trees Red/Black Trees Summary Chapter 12. Optimizing IO Efficient Screen Output Efficient Binary File IO Efficient Text File IO Summary Chapter 13. Optimizing Your Code Further Arithmetic Operations Operating System–Based Optimizations Summary Part III: Tips and Pitfalls Chapter 14. Tips Tricks Preparing for the Future Chapter 15. Pitfalls Algorithmic Pitfalls Typos that Compile Other Pitfalls
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敦兮其若朴,旷兮其若谷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值