C++编译器会优化重复的函数调用吗?

9

编译器是否会对重复的函数调用进行优化?

例如,考虑以下情况。

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。

1
试一试,看看... - M.M
诚恳的建议:只有在有所不同的情况下才进行测量。如果您无法测量出差异,则其并不重要。 - Baum mit Augen
除非函数以非标准的方式定义为纯函数(没有副作用),否则不会返回。 - StenSoft
我对这里的理论方面更感兴趣,而不是是否可以依靠它来使现实世界的代码运行更快。我特别关注在Linux上的x86_64架构下的GCC。这变得有趣了。我们是在讨论使用-flto还是不使用? - Baum mit Augen
1
@SU3 好的,已点赞并收藏。我现在无法查看此问题,但如果明天没有好的答案,我会研究一下。 - Baum mit Augen
显示剩余7条评论
3个回答

7

如果您启用了链接时优化并且优化级别足够高,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生成了实际的机器代码,它就不会真正知道此时安全做什么。例如,您的函数可能修改数据(在类外部)或访问一个易失性变量。
编辑:
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]

那么,你是说GCC即使没有属性也会尝试证明一个函数是纯的吗?除了lto之外,你知道需要开启哪些优化选项吗? - SU3
@SU3 我可以想到两种可能发生的情况。首先,GCC 可能会内联该函数(-finline-functions),然后进行公共子表达式消除(-fgcse)。它也可以按照您建议的正确标记该函数。我现在不熟悉并且找不到它所使用的机制,但我已经阅读过相关内容。 - Nick Apperson

0
编译器无法跨编译单元进行查看,因此在调用站点无法确定调用是否具有副作用,因此优化它是不正确的。

6
取决于情况。LTO(长期运营)在一定程度上可以。 - Baum mit Augen
1
原则上,可以在函数声明中指定纯属性,以允许编译器假定没有副作用发生。例如 [[ gnu::pure ]] - SU3
@BaummitAugen 当然可以,但问题是关于编译器的。 - user207421

0

除非函数及其与第一次和最后一次调用该函数之间的所有函数都被声明为纯函数(即没有任何副作用),否则编译器无法优化这些调用。请注意以下内容:

int test();
void some(int a);
void more(int b);

int main()
{
    some(test());
    more(test());
}

在这里,test 可能会被调用两次,因为它可以返回不同的值(LTO 可以通过内联“足够简单”的函数来优化此过程)。如果您希望编译器能够优化这些调用,它需要知道 testsome 都是纯函数,即对于 more(test()) 调用 test 不能返回与调用 some(test()) 时不同的值。因此,以下代码可以被优化为单个对 test 的调用(在 GCC 和 Clang 中也会这样做):

int test() __attribute__ ((pure));
void some(int a) __attribute__ ((pure));
void more(int b);

int main()
{
    some(test());
    more(test());
}

(请注意,more 不需要是纯的。)

不幸的是,目前还没有标准的方法来声明一个函数为纯函数,上述是非标准的 GCC 扩展。有提案 N3744来将 [[pure]] 添加到 ISO C++(在这种情况下,对于纯度更强的函数,some 不需要是纯的),但我不知道它是否会被加入到 C++17 中。


有人知道如果没有提供属性,GCC能够确定函数是否是纯函数的程度吗? - SU3
这里,测试(test)总是保证被调用恰好两次的说法是错误的。大多数链接器都足够智能,可以进行内联操作,此时它可以重新排列代码以实现调用1次甚至0次。 - Mooing Duck
@Mooing Duck,你能说出这样的链接器名称吗?我已经使用了GCC和Clang测试了这段代码,但它们都没有。 - StenSoft
@StenSoft:我敢打赌两者都会这样做。将test()的主体更改为{return 3;},然后看它被“调用”了多少次。 - Mooing Duck
这个函数非常简单,当有两个 mov $0x3,%edi 来准备参数时,很难判断它是一个调用还是两个调用。 - StenSoft
2
@MooingDuck 好的,我成功把它做到了那个程度,调用被优化为一个调用。(-flto 在 -O3 上默认没有启用,这就是导致我的原始测试未能捕获此问题的原因) 很高兴学到了新东西! - StenSoft

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