编译器(通常或特别是)优化重复的函数调用吗?
例如,考虑这种情况。
struct foo {
member_type m;
return_type f() const; // returns by value
};
功能定义在一个翻译单元
中return_type foo::f() const {
/* do some computation using the value of m */
/* return by value */
}
重复的函数调用在另一个单元中
foo bar;
some_other_function_a(bar.f());
some_other_function_b(bar.f());
第二个翻译单元中的代码会转换为此吗?
foo bar;
const return_type _tmp_bar_f = bar.f();
some_other_function_a(_tmp_bar_f);
some_other_function_b(_tmp_bar_f);
潜在地,计算f
确实很昂贵,但返回的类型可能非常小(考虑返回double
的数学函数)。编译器会这样做吗?是否有他们做或不做的情况?您可以考虑此问题的通用版本,而不仅仅是成员函数或没有参数的函数。
根据@ BaummitAugen的建议澄清:
我对这个问题的理论方面更感兴趣,而不是在是否可以依靠这个来使现实世界的代码运行得更快。我对使用Linux的x86_64上的GCC特别感兴趣。
答案 0 :(得分:3)
如果您启用了链接时间优化并且优化级别足够高,GCC绝对会优化编译单元,请参见此处:https://gcc.gnu.org/wiki/LinkTimeOptimization除编译时间外,没有理由不执行这两项操作。
此外,您始终可以通过使用适当的属性标记函数来帮助编译器。您可能希望使用属性const标记该函数,如下所示:
struct foo {
member_type m;
return_type f() const __attribute__((const)); // returns by value
};
在此处查看GCC文档,了解哪个属性合适:https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
从更一般的意义上讲,这对编译器来说非常容易检测。它实际上执行的变换不那么明显。但是,链接时间优化很重要的原因是,一旦GCC生成了实际的机器代码,它就不会真正知道在那一点上做什么是安全的。例如,您的函数可以修改数据(在类外)或访问volatile变量。
编辑:
GCC绝对可以做到这一优化。使用此代码和标志-O3 -fno-inline:
C ++代码:
#include <iostream>
int function(int c){
for(int i = 0; i != c; ++i){
c += i;
}
return c;
}
int main(){
char c;
::std::cin >> c;
return function(c) + function(c) + function(c) + function(c) + function(c);
}
装配输出:
4006a0: 48 83 ec 18 sub rsp,0x18
4006a4: bf 80 0c 60 00 mov edi,0x600c80
4006a9: 48 8d 74 24 0f lea rsi,[rsp+0xf]
4006ae: e8 ad ff ff ff call 400660 <_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_RS3_@plt>
4006b3: 0f b6 7c 24 0f movzx edi,BYTE PTR [rsp+0xf]
4006b8: e8 13 01 00 00 call 4007d0 <_Z8functioni>
4006bd: 48 83 c4 18 add rsp,0x18
4006c1: 8d 04 80 lea eax,[rax+rax*4]
4006c4: c3 ret
4006c5: 66 66 2e 0f 1f 84 00 data32 nop WORD PTR cs:[rax+rax*1+0x0]
4006cc: 00 00 00 00
但是,当函数位于单独的编译单元中且未指定-flto选项时,它确实无法执行此操作。只是为了澄清,这一行称为函数:
call 4007d0 <_Z8functioni>
此行将结果乘以5(加上五个副本):
lea eax,[rax+rax*4]
答案 1 :(得分:1)
编译器无法查看编译单元,因此无法在调用站点看出调用是否存在副作用,因此优化它是不正确的。
答案 2 :(得分:0)
除非函数的第一次和最后一次调用之间的函数和所有函数都被声明为纯(即没有任何副作用),否则编译器无法优化调用。请注意以下事项:
int test();
void some(int a);
void more(int b);
int main()
{
some(test());
more(test());
}
在这里,test
可能会被调用两次,因为它可以返回不同的值(LTO可以通过内联引用来优化它:“简单的”函数)。如果您希望编译器能够优化调用,则需要知道test
和some
都是纯粹的,即test
的{{1}}调用不可能返回不同价值比为more(test())
调用test
时的值。因此,可以对some(test())
的单个调用进行优化(并且将在GCC和Clang中进行优化):
test
(请注意int test() __attribute__ ((pure));
void some(int a) __attribute__ ((pure));
void more(int b);
int main()
{
some(test());
more(test());
}
不一定是纯粹的。)
不幸的是,还没有任何标准方法可以将函数声明为纯函数,上面是非标准的GCC扩展。将more
添加到ISO C ++中会有proposal N3744(对纯度有更强的保证,[[pure]]
不需要纯净,但我不知道它是否会不管是不是C ++ 17。