【问题标题】:g++ -O3 creates strange instructions for loopg++ -O3 为循环创建奇怪的指令
【发布时间】:2020-04-16 14:08:49
【问题描述】:

我正在使用 c++ 编写一些用于数值计算的代码。我需要非常仔细地编写代码来帮助编译器生成好的指令。然后,我发现带有 -O3 标志的 g++ 9.2 有一些奇怪的地方。我不是组装专家,所以我需要有人帮助我或指出我错在哪里。

完整的代码可以在这里找到https://godbolt.org/z/fyuYtq。我在这里复制并粘贴密钥sn-p

void sum_twopointer(Elem *p1, Elem *p2, ptrdiff_t stride, ptrdiff_t start, ptrdiff_t end) {

    Elem sm = 0;
    for(auto i = start;i != end; ++i) {
        p1[0] = p2[0] + p2[0];
        p1 += stride;
        p2 += stride;
    }

}

它是用g++ -O3 编译的。 g++ 的版本是 9.2。汇编代码是

sum_twopointer(double*, double*, long, long, long):
  cmp rcx, r8
  je .L32
  lea r9, [0+rdx*8]
  xor eax, eax
  cmp rdx, 1
  jne .L36
.L34:
  movsd xmm0, QWORD PTR [rsi+rax]
  add rcx, 1
  addsd xmm0, xmm0
  movsd QWORD PTR [rdi+rax], xmm0
  add rax, r9
  cmp r8, rcx
  jne .L34
.L32:
  ret
.L36:
  movsd xmm0, QWORD PTR [rsi+rax]
  add rcx, 1
  addsd xmm0, xmm0
  movsd QWORD PTR [rdi+rax], xmm0
  add rax, r9
  cmp r8, rcx
  jne .L36
  ret

据我了解,编译器正在尝试对 stride 为 1 的特殊情况进行一些优化,因此它为 stride==1 的情况创建了一个新分支,但它没有做任何事情进一步。请注意,.L34 后面的代码与 .L36 后面的代码完全相同

我为此做了一些基准测试。 stride=1 和 stride=2 的性能如下所示。代码在那里https://gist.github.com/lhprojects/dac3a9fcf15bd5b1ec365ba6a87c679d

g++ -O2
---------------------------------------------------------------
Benchmark                     Time             CPU   Iterations
---------------------------------------------------------------
BM_twopointer/8192/1       3743 ns         3742 ns       185062      stride=1
BM_twopointer/8192/2       1980 ns         1980 ns       328523      stride=2

g++ -O3
---------------------------------------------------------------
Benchmark                     Time             CPU   Iterations
---------------------------------------------------------------
BM_twopointer/8192/1       5006 ns         5001 ns       120725      stride=1
BM_twopointer/8192/2       2043 ns         2041 ns       333914      stride=2

无论如何,对于 stride=1,与 -O2 相比,使用 -O3 的性能会变差。我想知道我的代码发生了什么。我是否在 C++ 中触发了一些未定义的行为?或者简单地说,g++中的代码优化存在缺陷。 (如果我的英文写作让你感到很困惑,我很抱歉。)

【问题讨论】:

  • 应该归咎的优化标志是-fversion-loops-for-strides,默认情况下使用-O3启用。您可以在-O3 之后使用-fno-version-loops-for-strides 禁用它。我不知道为什么它会应用循环版本控制,即使它没有任何好处。
  • 不是上面代码的真正答案:但是代码的执行方式取决于许多因素,并且很难预测为什么代码在不同的编译选项下运行得更快/更慢以及为什么O2O3 并不总是有预期的结果。虽然编译器做了令人难以置信的优化工作,但它们并不总是正确的,但任意基准测试的执行也可能与实际用例不同,因此很难判断编译器是否真的错了。有趣的话题是StrangeLoop "Performance Matters" by Emery Berger
  • @t.niese 感谢您的评论。我同意更多的优化标志/更高的优化级别并不总是会生成更好的代码。我们有无限的情况。但我认为这里的代码缺陷很明显。两个分支是相同的代码。我觉得这太奇怪了。
  • @walnut 谢谢你的评论。我现在学会了一个有用的技巧。

标签: c++ g++ compiler-optimization


【解决方案1】:

我相信编译器需要知道 p1 和 p2 不重叠......将它们声明为 __restrict 指针应该允许编译器实际使用 simd 指令。对我来说,它会为 stride==1 创建一个特殊情况,但对我来说似乎很奇怪,但不会对这些知识做任何事情。

【讨论】:

  • 刚才我给p1和p2加了__restrict,但是好像没什么变化。见godbolt.org/z/7Rqvna。而且我认为用文字'8'替换r9寄存器仍然是安全的,即使两个指针重叠。
猜你喜欢
  • 1970-01-01
  • 2017-09-08
  • 1970-01-01
  • 2020-10-26
  • 2013-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多