前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >现代CPU性能分析与优化-性能分析方法-编译器优化报告​

现代CPU性能分析与优化-性能分析方法-编译器优化报告​

作者头像
王很水
发布2024-08-20 20:24:25
1130
发布2024-08-20 20:24:25
举报
文章被收录于专栏:C++ 动态新闻推送

如今,软件开发在很大程度上依赖编译器进行性能优化。编译器在加速软件方面扮演着关键角色。大多数开发人员将优化代码的工作留给编译器,只有当他们发现编译器无法完成的优化机会时才会干预。可以说,这是一个好的默认策略。但是,当您追求最佳性能时,它就不太管用了。如果编译器没有执行关键优化,例如向量化循环,怎么办?您将如何知道这一点?幸运的是,所有主流编译器都提供优化报告,我们现在将讨论这些报告。

假设您想知道一个关键循环是否被展开。如果是,展开因子是多少?有一种艰苦的方法可以知道这一点:研究生成的汇编指令。不幸的是,并不是每个人都习惯于阅读汇编语言。如果函数很大,它调用其他函数或也有许多被向量化的循环,或者如果编译器为同一个循环创建了多个版本,这可能会特别困难。大多数编译器,包括 GCC、Clang 和 Intel 编译器(但不包括 MSVC),都提供优化报告,用于检查特定代码段执行了哪些优化。

让我们看一下 下面的代码,它展示了一个由 clang 16.0 未向量化的循环示例。

代码语言:javascript
复制
void foo(float* __restrict__ a, 
         float* __restrict__ b, 
         float* __restrict__ c,
         unsigned N) {
  for (unsigned i = 1; i < N; i++) {
    a[i] = c[i-1]; // value is carried over from previous iteration
    c[i] = b[i];
  }
}
代码语言:javascript
复制

在 clang 中生成优化报告,您需要使用 -Rpass*: https://llvm.org/docs/Vectorizers.html#diagnostics 标志:

代码语言:javascript
复制
$ clang -O3 -Rpass-analysis=.* -Rpass=.* -Rpass-missed=.* a.c -c
a.c:5:3: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
  for (unsigned i = 1; i < N; i++) {
  ^
a.c:5:3: remark: unrolled loop by a factor of 8 with run-time trip count [-Rpass=loop-unroll]
  for (unsigned i = 1; i < N; i++) {
  ^
代码语言:javascript
复制

检查上面的优化报告,我们可以看到循环没有被向量化,而是被展开了。开发人员并不总是很容易识别 @lst:optReport 第 6 行循环中是否存在循环进位依赖。由 c[i-1] 加载的值取决于前一次迭代的存储(参见图 @fig:VectorDep 中的操作 \circled{2} 和 \circled{3})。可以通过手动展开循环的前几个迭代来揭示依赖关系:

代码语言:javascript
复制
// iteration 1
  a[1] = c[0];
  c[1] = b[1]; // writing the value to c[1]
// iteration 2
  a[2] = c[1]; // reading the value of c[1]
  c[2] = b[2];
...
代码语言:javascript
复制

如果我们将 @lst:optReport 中的代码向量化,它会导致在数组 a 中写入错误的值。假设 CPU SIMD 单元可以一次处理四个浮点数,我们可以得到可以用以下伪代码表示的代码:

代码语言:javascript
复制
// iteration 1
  a[1..4] = c[0..3]; // oops!, a[2..4] get wrong values
  c[1..4] = b[1..4]; 
...
代码语言:javascript
复制

@lst:optReport 中的代码无法向量化,因为循环内部的操作顺序很重要。如 @lst:optReport2 所示,通过交换第 6 行和第 7 行可以修复此示例。这不会改变代码的语义,所以这是一个完全合法的更改。另外,可以通过将循环拆分成两个单独的循环来改善代码。

代码清单:a.c

代码语言:javascript
复制
void foo(float* __restrict__ a, 
         float* __restrict__ b, 
         float* __restrict__ c,
         unsigned N) {
  for (unsigned i = 1; i < N; i++) {
    c[i] = b[i];
    a[i] = c[i-1];
  }
}
代码语言:javascript
复制

在优化报告中,我们可以看到循环成功向量化了:

代码语言:javascript
复制
$ clang -O3 -Rpass-analysis=.* -Rpass=.* -Rpass-missed=.* a.c -c
a.cpp:5:3: remark: vectorized loop (vectorization width: 8, interleaved count: 4) [-Rpass=loop-vectorize]
  for (unsigned i = 1; i < N; i++) {
  ^
代码语言:javascript
复制

这只是使用优化报告的一个例子,我们将在本书的第二部分讨论发现向量化机会时更详细地介绍。编译器优化报告可以帮助您找到错过的优化机会,并了解这些机会错过的原因。此外,编译器优化报告对于测试假设很有用。编译器通常会根据其成本模型分析来决定某个转换是否有益。但编译器并不总是做出最佳选择。一旦您在报告中发现缺少关键优化,您可以尝试通过更改源代码或向编译器提供提示(例如 #pragma、属性、编译器内置函数等)来纠正它。始终通过在实际环境中进行测量来验证您的假设。

编译器报告可能相当庞大,每个源代码文件都会生成单独的报告。有时,在输出文件中找到相关记录可能成为一项挑战。我们应该提到,最初这些报告的设计明确供编译器编写者用于改进优化过程。多年来,已经出现了一些工具,使它们更易于应用程序开发人员访问和操作。最值得注意的是 opt-viewer7 和 optview28。此外,Compiler Explorer 网站还为基于 LLVM 的编译器提供了“优化输出”工具,当您将鼠标悬停在源代码相应行上时,它会报告执行的转换。所有这些工具都帮助可视化基于 LLVM 的编译器成功的和失败的代码转换。

在 LTO5 模式下,一些优化是在链接阶段进行的。为了同时从编译和链接阶段发出编译器报告,应该向编译器和链接器传递专用选项。有关更多信息,请参见 LLVM "remarsk"6指南。

Intel® ISPC3 编译器 (已在 [@sec:ISPC] 中讨论) 采用稍微不同的方式报告缺失的优化。它会针对编译为相对低效代码的代码结构发出警告。无论哪种方式,编译器优化报告都应该是您工具箱中的关键工具之一。它是一种快速的方法,可以检查对特定热点进行了哪些优化,以及是否失败了一些重要的优化。许多改进机会都是通过编译器优化报告发现的。

1. 使用编译器优化指令 - https://easyperf.net/blog/2017/11/09/Multiversioning_by_trip_counts ↩

3. ISPC - https://ispc.github.io/ispc.html ↩

5. 链接时间优化,也称为过程间优化(IPO)。阅读更多: https://en.wikipedia.org/wiki/Interprocedural_optimization ↩

6. LLVM compiler remarks - https://llvm.org/docs/Remarks.html ↩

7. opt-viewer - https://github.com/llvm/llvm-project/tree/main/llvm/tools/opt-viewer ↩

8. optview2 - https://github.com/OfekShilon/optview2 ↩

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CPP每周推送 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档