为什么编译器不总是优化掉局部变量?

Rac*_*box 6 c++ gcc micro-optimization compiler-optimization

我试图了解是否删除局部中间变量可以导致更好的优化代码。考虑以下MWE,要特别注意这两个函数fg

struct A {
    double d;
};

struct B {
    double s;
};

struct C {
    A a;
    B b;
};

A geta();
B getb();

C f() {
    const A a = geta();
    const B b = getb();

    C c;
    c.a = a;
    c.b = b;
    return c;
}

C g() {
    C c;
    c.a = geta();
    c.b = getb();
    return c;
}
Run Code Online (Sandbox Code Playgroud)

fg呼叫geta()getb()来填充类的实例C,然后将其返回,但f使用两个本地中间变量来存储的返回值geta()getb(),而g直接返回值分配给成员c

与编译gcc -O3,9.2版本,这两个功能的二进制文件f,并g是完全一样的。但是,向AB类中添加另一个变量会导致不同的二进制文件。特别是,二进制文件for f具有更多指令。带-O3标志的clang v8.0.0的情况相同。

这是怎么回事 为什么编译器无法优化fwhen 的局部中间变量AB变得更复杂?是不是fg等价的代码?


此外,该行为是不是MSVC v19.22用相同的/O2标志:从微软编译器在第一种情况下,即与两个班已经有不同的二进制文件A,并B通过一个单一的组成double

我正在使用Godbolt:您可以在此处找到产生不同二进制代码的代码。

Pet*_*des 5

这是一个错过的优化

这两个函数都没有获取 的地址C c,因此转义分析应该很容易证明它是一个纯本地函数,没有其他函数可以有指针指向。 geta()并且getb()无法直接读取或写入该变量,因此将geta()返回值直接存储到c.a堆栈中而不是临时存储在堆栈中是安全的。

令人惊讶的是,GCC、clang、ICC 和 MSVC 都错过了这种优化,大多数使用调用保留寄存器来保存返回geta()值,直到 之后getb()https://godbolt.org/z/WQ9MAF 至少对于 x86-64;我基本上没有检查其他 ISA 或较旧的编译器版本。

有趣的事实:clang 3.5 即使对于 也有这种错过的优化g(),破坏了源代码提高效率的尝试。

有趣的事实#2:在 GCC9.2 中,编译为 C 而不是 C++ 会使 GCC 的工作变得更糟糕,从而导致g(). (我必须更改为typedef struct Atag {...} A;但编译它,因为 C++ 仍然优化g()。https ://godbolt.org/z/_Y95nj

clang8.0 产生高效的g()有/无-xc. 无论哪种方式,ICC 都会产生低效g()

ICC的f()比它的还要糟糕g()


MSVC 的g()效率正如您所希望的那样;Windows x64 调用约定通过隐藏指针返回结构,MSVC 从未优化它以将指针传递给其自己的返回值对象。(如果它自己的调用者也可能进行此类优化,那么它可能无法证明它是安全的。)


显然,如果geta()getb()可以内联,这就消除了优化器的任何疑问,并且它应该更容易/可靠地进行优化。