为什么编译器并不总是优化掉局部变量?
Why does the compiler not always optimize away local variables?
我想了解删除局部中间变量是否会导致更好的优化代码。考虑以下 MWE,特别注意两个函数 f
和 g
:
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;
}
f
和 g
都调用 geta()
和 getb()
来填充 class C
的实例然后返回,但是f
使用两个局部中间变量存储geta()
和getb()
的返回值,而g
直接将返回值赋值给c
的成员。
使用 gcc -O3
版本 9.2 编译,f
和 g
这两个函数的二进制文件完全相同。但是,将另一个变量添加到 A
或 B
class 会导致 不同的 二进制文件。特别是,f
的二进制文件有更多的指令。带有 -O3
标志的 clang v8.0.0 也是如此。
这里发生了什么?当 A
或 B
变得更复杂时,为什么编译器无法优化掉 f
的局部中间变量? f
和g
的代码不是等价的吗?
此外,带有 /O2
标志的 MSVC v19.22 的行为不同:在第一种情况下,Microsoft 的编译器已经有不同的二进制文件,即两个 classes A
和 B
由单个 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()
可以内联,这就消除了对优化器的任何疑问,它应该更容易/可靠地进行优化。
我想了解删除局部中间变量是否会导致更好的优化代码。考虑以下 MWE,特别注意两个函数 f
和 g
:
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;
}
f
和 g
都调用 geta()
和 getb()
来填充 class C
的实例然后返回,但是f
使用两个局部中间变量存储geta()
和getb()
的返回值,而g
直接将返回值赋值给c
的成员。
使用 gcc -O3
版本 9.2 编译,f
和 g
这两个函数的二进制文件完全相同。但是,将另一个变量添加到 A
或 B
class 会导致 不同的 二进制文件。特别是,f
的二进制文件有更多的指令。带有 -O3
标志的 clang v8.0.0 也是如此。
这里发生了什么?当 A
或 B
变得更复杂时,为什么编译器无法优化掉 f
的局部中间变量? f
和g
的代码不是等价的吗?
此外,带有 /O2
标志的 MSVC v19.22 的行为不同:在第一种情况下,Microsoft 的编译器已经有不同的二进制文件,即两个 classes A
和 B
由单个 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()
可以内联,这就消除了对优化器的任何疑问,它应该更容易/可靠地进行优化。