GCC似乎错过了简单的优化

6
我正在尝试引入一个通用函数,其语义类似于三元运算符:E1 ? E2 : E3。我发现编译器能够根据三元运算符的条件消除其中一个E2E3的计算。然而,在调用ternary函数时,即使E2/E3没有副作用,GCC也会错过这种优化。
在下面的代码清单中,函数ternary被编写成与三元运算符类似。但是,GCC会发出潜在的重复调用函数f的指令,这似乎可以通过消除一些输入值(就像对于三元运算符一样)来实现,因为f被声明为纯属性,请查看godbolt链接以获取由GCC生成的汇编代码。
这是否是GCC可以改进的地方(优化的空间),还是C++标准明确禁止这种优化?
// Very heavy function
int f() __attribute__ ((pure));

inline int ternary(bool cond, int n1, int n2) {
    return cond ? n1 : n2;
}

int foo1(int i) {
    return i == 0 ? f() : 0;
}

int foo2(int i) {
    return ternary(i == 0, f(), 0);
}

使用-O3 -std=c++11的汇编清单:

foo1(int):
  test edi, edi
  jne .L2
  jmp f()
.L2:
  xor eax, eax
  ret
foo2(int):
  push rbx
  mov ebx, edi
  call f()
  test ebx, ebx
  mov edx, 0
  pop rbx
  cmovne eax, edx
  ret

https://godbolt.org/z/HfpNzo


2
它可以使用__attribute__ ((const))来“工作”。 - melpomene
2
@eXX:你应该在GCC的bugzilla上报告这个关键字=missed-optimization的错误。https://gcc.gnu.org/bugzilla/enter_bug.cgi?product=gcc。 - Peter Cordes
1
嘿,如果我将 foo1 改为 int t = f(); return i == 0 ? t : 0;,GCC 就不会对其进行优化(它总是调用 f),但是它会将 foo2 编译成 jmp foo1 - melpomene
3
让此事棘手的一点是,完全没有迹象表明函数调用是昂贵的,而有迹象表明调用函数是昂贵的:这需要一个额外的分支。 - user743382
@hvd 说得好,但是“多个分支”不是一个合理的估算函数调用成本的方法吗? - 463035818_is_not_a_number
显示剩余6条评论
1个回答

8
我发现编译器可以根据E1的条件(只要E2 / E3没有副作用)消除E2或E3中的一个计算,对于三元运算符。

编译器不会消除它; 它只是从一开始就没有将其优化为 cmov C ++抽象机器不会评估未使用的三元运算符的一侧。

int a, b;
void foo(int sel) {
    sel ? a++ : b++;
}

如下所示编译(Godbolt):

foo(int):
    test    edi, edi
    je      .L2                # if(sel==0) goto
    add     DWORD PTR a[rip], 1   # ++a
    ret
.L2:
    add     DWORD PTR b[rip], 1   # ++b
    ret

当输入没有任何副作用时,三目运算符可以优化为一个汇编cmov,否则它们并不完全等价。
在C++的抽象机器中(即gcc优化器的输入),foo2总是调用f(),而foo1不会。因此foo1被编译成这样并不奇怪。
要使foo2以这种方式编译,必须优化掉对f()的调用。它总是被调用来创建ternary()的参数。
这里存在一个遗漏的优化,你应该在GCC的bugzilla上报告(使用missed-optimization关键字作为标记)。https://gcc.gnu.org/bugzilla/enter_bug.cgi?product=gcc 调用int f() __attribute__ ((pure));应该能够被优化掉。它可以读取全局变量,但不能有任何副作用。(https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
正如评论中@melpomene发现的那样,int f() __attribute__ ((const));可以给您所寻找的优化。一个__attribute__((const))函数甚至不能读取全局变量,只能读取它的参数。(因此,如果没有参数,它必须始终返回常量。)
HVD指出,gcc对f()没有任何成本信息。即使它可以像对((const)) f一样优化掉调用((pure)) f(),也许它没有这样做是因为它不知道它比条件分支更昂贵?可能使用基于配置文件的优化会使gcc采取行动吗?
但是,考虑到它在foo2中将对((const)) f()的调用作为条件语句,gcc可能不知道它可以优化掉对((pure))函数的调用?也许它只能对它们进行公共子表达式消除(如果没有写入全局变量),而不能完全从基本块中优化掉?或者当前的优化器只是未能利用好。就像我说的,看起来是一个遗漏的优化bug。

1
一个观察是,如果i == 0为真,并且使用了f()的值,那么它必须是在抽象机器调用它的位置调用f()时产生的值。在这种情况下没有区别,但是如果在两者之间写入了任何全局变量或逃逸的局部变量,它们可能会影响f()返回的值(因为pure函数可以读取它们)。也就是说,编译器不能将对pure函数的调用重新排序到任何全局变量的写入之后。因此,它可能不太愿意尝试重新排序pure调用。 - undefined
此外,我认为编译器发现这种优化的自然方式是尝试将无条件调用 f() 推迟到分支之后,从而将表达式有效地转换为 i == 0 ? f() : (f(), 0)。然后它可以注意到,由于 f 是纯函数,(f(), 0) 可以被替换为 0。但第一步(推迟)本身有些不自然,因为在大多数情况下,你会期望这种转换会导致更糟糕的代码。所以也许这个优化没有被尝试。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接