一些C/C++优化工具

2021-11-13

性能数据收集

perf

Linux下收集性能数据通常用perf。关于perf的使用可以参考Brendan Gregg的网站

其中最常用的命令是perf record和perf report,而常用的指标有cycles、branch miss、cache miss等。

然后用FlameGraph工具可以生成火焰图,直观显示性能消耗在何处。

perf的原理

收集cycles、branch miss、cache miss这些数据,主要应用到了CPU当中的 Performance Monitoring Unit(简称PMU)。PMU其实就是CPU当中的一个计数器,当发生特定事件的时候加一,并可以向内核报告寄存器状态,尤其是Program Counter寄存器的状态,这样就可以知道branch miss、cache miss发生在何处。

因为会带来性能损耗,所以使用PMU的时候一般采用采样模式。例如,每发生一万次cache miss才会报告一次。

此外,Intel的CPU还有一项功能,叫Least Recent Branch(简称LRB),可以记录控制流,也被perf用于发现程序中的热点。

关于PMU和LRB的原理,参见:

手工优化

关于怎么优化程序,有一些放之四海而皆准的老生常谈,比如如何优化系统调用、如何用锁等等、使用-O3编译选项等等。不过,也有一些更精妙的东西。

其一是likely/unlikely宏,而这些宏依赖了GCC中的__builtin_expect函数。如下:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

这个宏可以告诉编译器在分支处更容易跳转到哪里。编译器根据这些提示,可以做出相应优化,防止发生太长的跳转,进而提高分支预测的成功率,并减少程序加载时出现page fault的可能性。

从C++20开始,likely和unlikely以attribute的形式进入了C++标准。使用方法如下:

double pow(double x, long long n) {
    if (n > 0) [[likely]]
        return x * pow(x, n - 1);
    else [[unlikely]]
        return 1;
}

此外,也有一些builtin功能可以提示编译器对缓存做更精确的控制,例如 __builtin_prefetch。

参见:

自动优化

上面的手工优化,实际操作起来往往很繁琐,所以只能在少量热点处使用。如果要对程序进行大量优化,免不了要使用自动化工具。

LTO

LTO全称是Link-Time Optimization,即“链接期优化”。LTO可以对程序进行全局优化,而不仅仅着眼于局部,故可以执行一些更激进的优化策略。但是缺点是优化方法比较复杂,且内存消耗大;针对这些缺点又有了Thin LTO。LTO使用起来也很简单,利用clang编译,然后在编译和链接时加上-flto和-flto=thin即可。

参见:

PGO

PGO,全名Profile-Guided Optimazation,即通过编译的时候,构建一个运行模型,通过Profile数据来指导编译器优化。目前GCC已经提供了该功能。但是缺点是这个过程通常非常耗时,而且不好收集数据集,构建模型也很复杂。针对此,谷歌做了一些PGO自动化相关的工作,开源了AutoFDO,全名Automatic Feedback-Directed Optimization,通过采集生产机器上的数据来做反馈优化。不过,谷歌的AutoFDO虽然开源了,但是Github上似乎并没有详细的使用说明,只能够找到一些零散的资料。

参见:

BOLT

BOLT由Facebook开发(现在改名meta了),也是一种反馈优化,全名是Binary Optimization and Layout Tool。根据Facebook的数据,BOLT给他们的Hack语言(一种php类似物)虚拟机HHVM带来了8%的性能提升。

参见:

正如其名,BOLT在优化时不需要源代码,可以直接操作二进制。由重排代码、优化控制流,而提高指令缓存的效率。如果程序依赖了一些没有源代码的二进制库,那么BOLT在这种场景下就有很大优势。



Email: i (at) mistivia (dot) com