为什么这个C++成员函数在使用-O3编译器优化时没有被优化?

9
在下面声明的C++ vector类中,norm成员函数被标记为const,并且(据我所知)不包含任何副作用。
template <unsigned int N>
struct vector {
  double v[N];

  double norm() const {
    double ret = 0;
    for (int i=0; i<N; ++i) {
      ret += v[i]*v[i];
    }
    return ret;
  }
};

double test(const vector<100>& x) {
  return x.norm() + x.norm();
}

如果我在使用gcc编译器(版本5.4)并开启优化(即-O3)的情况下多次调用vectorconst实例上的norm函数(参见上面的test函数),则编译器会内联norm,但仍会多次计算norm的结果,尽管结果不应该改变。为什么编译器不能优化第二次对norm的调用,只计算一次结果?这个答案似乎表明,如果编译器确定norm函数没有任何副作用,那么编译器应该执行此优化。为什么在这种情况下不会发生这种情况?
请注意,我使用Compiler Explorer来确定编译器生成的内容,并且下面给出了gcc版本5.4的汇编输出。clang编译器也会给出类似的结果。还要注意,如果我使用gcc的编译器属性手动标记norm为const函数,使用__attribute__((const)),则第二次调用会被优化掉,这正是我想要的,但我的问题是,为什么gcc(和clang)不会自动执行此操作,因为norm的定义是可用的?
test(vector<100u>&):
        pxor    xmm2, xmm2
        lea     rdx, [rdi+800]
        mov     rax, rdi
.L2:
        movsd   xmm1, QWORD PTR [rax]
        add     rax, 8
        cmp     rdx, rax
        mulsd   xmm1, xmm1
        addsd   xmm2, xmm1
        jne     .L2
        pxor    xmm0, xmm0
.L3:
        movsd   xmm1, QWORD PTR [rdi]
        add     rdi, 8
        cmp     rdx, rdi
        mulsd   xmm1, xmm1
        addsd   xmm0, xmm1
        jne     .L3
        addsd   xmm0, xmm2
        ret

4
“const似乎不对,我认为使用pure更好,但是这样优化就不会发生。”这是一个关于优化顺序的问题。如果函数被标记为const,则x+x会很早被优化为2*x(x仅出现一次)。否则,如果将函数标记为noinline,则第二次调用会被优化为冗余。但是如果先进行内联,则编译器更难以注意到两个循环正在计算相同的内容(合并循环可能是朝着这个方向的一个潜在有用的步骤)。 - Marc Glisse
1
传递向量通过值时会发生相同的情况吗?仅仅因为你有一个常量引用并不意味着它在其他地方没有被修改。常量引用仅仅是一个信号,表明被调用者(而非调用者)不会改变所引用的对象。在一般情况下,我认为GCC优化掉第二次调用是错误的,尽管你添加了__attribute__((const)),但它可能已经进行了更复杂的分析,并确定在有限的测试案例中可以省略第二次调用。 - Jarra McIntyre
2
我猜在没有正确使用互斥锁的环境中,另一个函数可能会在调用“norm”的过程中更改数组中的内容。 - JHBonarius
更有趣的是编译器正在使用SSE进行优化,但是双倍精度浮点数的乘法一次只处理一个(mulsd)而不是两个(mulpd)。这进一步证明了如果你有时间关键的代码,就不要依赖于编译器去做"正确的事情"。 - BitBank
@J.H.Bonarius:那将是未定义的行为。 - Karoly Horvath
1个回答

4
编译器可以计算norm的结果并多次重复使用。例如,使用-Os开关:
test(vector<100u> const&):
        xorps   xmm0, xmm0
        xor     eax, eax
.L2:
        movsd   xmm1, QWORD PTR [rdi+rax]
        add     rax, 8
        cmp     rax, 800
        mulsd   xmm1, xmm1
        addsd   xmm0, xmm1
        jne     .L2
        addsd   xmm0, xmm0
        ret

缺失的优化与非关联浮点数学一些可观察行为问题无关。


在没有正确使用互斥锁的环境中,另一个函数可能会在 norm 调用之间更改数组中的内容。
这种情况可能会发生,但编译器并不关心(例如:https://stackoverflow.com/a/25472679/3235496)。

使用 -O2 -fdump-tree-all 开关编译示例,您可以看到:

  • g++ 正确将 vector<N>::norm() 检测为纯函数(输出文件 .local-pure-const1);
  • 内联发生在早期阶段(输出文件 .einline)。

还要注意,使用 __attribute__ ((noinline)) 标记 norm编译器执行CSE

test(vector<100u> const&):
    sub     rsp, 8
    call    vector<100u>::norm() const
    add     rsp, 8
    addsd   xmm0, xmm0
    ret

马克·格利斯(可能)是正确的。

需要一种更高级的公共子表达式消除取消内联循环表达式


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