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

7

我想了解如果删除本地中间变量是否可以导致更优化的代码。请考虑以下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()来填充一个C类的实例,然后返回该实例,但是f使用两个本地中间变量来存储geta()getb()的返回值,而g直接将返回值分配给c的成员。

使用gcc -O3,版本9.2编译,两个函数fg的二进制代码完全相同。然而,向AB类添加另一个变量会导致二进制代码不同。特别是,f的二进制代码有一些额外的指令。对于clang v8.0.0也是如此,使用-O3标志。

这里发生了什么?为什么编译器不能在AB变得更加复杂时优化掉f的本地中间变量?fg的代码不等价吗?

此外,使用/O2标志的MSVC v19.22的行为也不相同:Microsoft的编译器在第一种情况下已经具有不同的二进制代码,即AB都由单个double组成。

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


1
编译器不会随意优化局部变量。编译器只能优化那些在您的代码中没有副作用的变量。AB调用函数,C(c)分配成员并返回值。根据您的片段很难确定您认为应该优化掉什么?进一步选择要取出的变量是单个变量。如果您有一个对象和该对象的几个成员(例如.a.b什么也不做),它们仍然不太可能被优化掉,因为它们不是独立的变量。 - David C. Rankin
4
如果你为getA()getB()提供简单的定义,那么f()g()的汇编代码将是相同的。https://godbolt.org/z/lOjAC- - Sebi
2
如果您向A添加微不足道的自定义赋值运算符,它将在gcc中使f()g()的机器代码再次相同... :D :D :D ...(要添加到struct A中的代码:A&operator =(const A&t){return * this;}....似乎默认赋值运算符正在执行某些“额外”的操作,这会防止折叠,或者仅仅是因为该运算符不是显式的,而gcc必须用默认运算符填补空白,从而防止优化器折叠它。....但是对于您的问题,一般答案实际上非常简单:“为什么不呢?”...这是优化器,而不是“找到最佳解决方案的器”。 - Ped7g
2
除非您想了解实现该机器代码所使用的精确逻辑,否则必须咨询gcc源代码,了解编译器和优化器的实现方式。如果您尝试一下,也许会意识到实际上有多少东西存在,以及对人类来说似乎微不足道的事情在实现中可能会变得更加复杂...最终,编译器应该在合理的有限时间内完成编译,而即使是简单的源代码,可能有数十万种或更多的可能的机器代码变体,只有一小部分被考虑到。 - Ped7g
2
(虽然这似乎是一个值得向gcc团队报告的错过优化的问题,或者他们可能会解释为什么它像这样工作) - Ped7g
显示剩余2条评论
1个回答

5

这是一个被错过的优化

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

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

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

有趣的事实#2:对于GCC9.2,将编译为C而不是C ++会使GCC表现得更差,使g()失去优化。 (我不得不更改为typedef struct Atag {...} A;,但将其编译为C ++仍会优化g()https://godbolt.org/z/_Y95nj

clang8.0使用/不使用-xc都会生成有效的g(),ICC则无论如何都会生成低效的g()

ICC的f()比其g()还要糟糕。


MSVC的g()是你可以希望的最有效率的; Windows x64调用约定通过隐藏指针返回结构体,并且MSVC从不将其优化为传递指向其自己的返回值对象的指针。(如果其调用者也可能进行这样的优化,它可能无法证明安全性。)


显然,如果geta()getb()可以内联,那么这消除了任何对优化器的怀疑,它应该更轻松/可靠地进行优化。


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