使用CodeQL在C++中寻找10倍以上性能提升:结合动态与静态分析进行性能优化

本文探讨如何结合动态和静态分析来优化C++代码性能,重点介绍使用CodeQL检测类型别名导致的性能问题,通过实际案例展示如何识别和修复这些问题,实现显著的性能提升。

使用CodeQL在C++中寻找10倍以上性能提升

结合动态与静态分析进行性能优化

在上一篇文章中,我提倡构建结合静态和动态分析的系统来进行性能优化。通过这种方式,我们可以构建比单独使用任一分析方法更有用的工具。事实上,对于许多静态分析来说,是否有用很可能取决于它是否与性能分析器结合使用。

类型别名在C/C++中的性能问题

C/C++中的char类型(及其类似类型,如uint8_t)可以别名化任何其他类型。在通过此类类型写入内存时,编译器必须假设它可能修改了任何其他基于堆的内存位置,即使该位置具有完全不同的类型。

这导致编译器无法安全地自动向量化循环,因为它需要在每次写入后重新加载这些值。在循环上下文中,这个问题可能导致编译器生成的代码比原本可能的代码慢10-20倍或更多。

使用CodeQL检测别名写入

CodeQL允许您以声明性面向对象语言编写查询,并将这些查询应用于已转换为数据库中关系表示的代码库。它提供了一种相对易于使用的查询语言,用于查找代码中的有趣模式。

我们构建了CodeQL查询来查找以下模式:

  1. 通过表达式X从内存加载,其中X表示内存位置
  2. 通过别名类型(如char)进行写入
  3. 再次通过X从内存加载,且表达式X的组件自步骤1以来未被修改

实际案例与性能提升

Bitcoin案例:bech32::ExpandHRP函数

在Bitcoin的bech32.cpp文件中,ExpandHRP函数包含一个循环,其中:

  • 循环条件调用std::stringsize()函数(从内存读取字符串长度)
  • 循环体内有对std::vector<uint8_t>的写入

由于uint8_t本质上是char类型,编译器必须在每次循环迭代时重新加载字符串长度和数据指针。通过将std::vector<uint8_t>替换为std::vector<char8_t>(C++20),我们消除了别名问题,使编译器能够向量化循环。

结果:在32k输入字节时,优化版本比原版本快12倍;在90字节(实际最大输入长度)时,优化版本快2.6倍。

Monero案例:bulletproof_PROVE函数

在Monero的bulletproofs.cc文件中,bulletproof_PROVE函数包含一个循环,其中对char类型的bytes数组进行多次赋值。每次赋值后,编译器必须重新加载多个先前加载的值。

通过将bytes数组的类型改为char8_t,编译器能够一次性执行所有8个赋值。

结果:对于大小为128到8192的输入,速度提升了3-4倍。

分析与结论

通过结合CodeQL静态分析和动态性能分析,我们能够:

  • 识别由类型别名引起的性能问题
  • 通过简单的类型更改实现显著的性能提升
  • 专注于最有可能产生影响的代码区域(热点循环)

这种方法特别适用于:

  • 循环中的内存访问模式
  • 使用内联函数调用(如vector.size())的循环条件
  • 包含通过字符类型写入的循环体

未来的工作可能包括:

  • 探索其他语言级性能问题的静态检测
  • 在汇编级别进行此类分析
  • 开发特定代码库的重复性能问题模式

结合静态分析和持续性能分析,为性能优化提供了一个强大的工具组合,使开发人员能够专注于最有影响力的改进。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计