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

Why does the compiler not always optimize away local variables?

我想了解删除局部中间变量是否会导致更好的优化代码。考虑以下 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;
}

fg 都调用 geta()getb() 来填充 class C 的实例然后返回,但是f使用两个局部中间变量存储geta()getb()的返回值,而g直接将返回值赋值给c的成员。

使用 gcc -O3 版本 9.2 编译,fg 这两个函数的二进制文件完全相同。但是,将另一个变量添加到 AB class 会导致 不同的 二进制文件。特别是,f 的二进制文件有更多的指令。带有 -O3 标志的 clang v8.0.0 也是如此。

这里发生了什么?当 AB 变得更复杂时,为什么编译器无法优化掉 f 的局部中间变量? fg的代码不是等价的吗?


此外,带有 /O2 标志的 MSVC v19.22 的行为不同:在第一种情况下,Microsoft 的编译器已经有不同的二进制文件,即两个 classes AB 由单个 double.

组成

我正在使用 Godbolt:您可以找到 here 生成不同二进制文件的代码。

这是一个遗漏的优化

两个函数都没有使用 C c 的地址,因此逃逸分析应该很容易证明它是一个纯本地函数,没有其他函数可以指向它。 geta()getb() 不能直接读取或写入该变量,因此将 geta() return 值直接存储到 c.a 而不是临时存储是安全的在堆栈上。

令人惊讶的是 GCC、clang、ICC 和 MSVC 都错过了这个优化,大多数使用调用保留寄存器来保存 geta() return 值直到 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() with/without -xc。并且 ICC 产生效率低下的 g() 无论哪种方式。

ICC 的 f() 甚至比它的 g() 还差。


MSVC 的 g() 是您所希望的高效; Windows x64 调用约定 return 是隐藏指针的结构,而 MSVC 从不优化它以将指针传递给它自己的 return 值对象。 (无论如何它可能无法证明是安全的,如果它自己的调用者也可能在做这样的优化。)


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