【问题标题】:Do C++ compilers optimize repeated function calls?C++ 编译器是否优化重复的函数调用?
【发布时间】:2017-06-26 20:35:54
【问题描述】:

编译器(通常或特别)是否优化重复的函数调用?

例如,考虑这种情况。

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 特别感兴趣。

【问题讨论】:

  • 试试看...
  • 诚实的建议:只要衡量它是否有所作为。如果您无法衡量差异,则差异不显着。
  • @M.M 好吧,那只会测试我能想到的几个特定场景。另外,我不太懂汇编。
  • 否,除非函数以非标准方式定义为纯(没有副作用)。
  • @SU3 好的,点赞并加星标。我现在无法调查,但如果明天之前没有好的答案,我会调查一下。

标签: c++ compiler-optimization


【解决方案1】:

编译器无法跨编译单元查看,因此无法在调用站点判断调用是否有副作用,因此将其优化掉是不正确的。

【讨论】:

  • 视情况而定。 LTO 在某种程度上可以。
  • 原则上可以在函数声明中指定纯属性,让编译器假设没有副作用发生吗?例如。 [[ gnu::pure ]].
  • @BaummitAugen 当然,但问题是关于编译器的。
【解决方案2】:

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

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

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

这里,test 可能会被调用两次,因为它可以返回不同的值(LTO 可以通过内联引号来优化这一点:“足够简单”的函数)。如果您希望编译器能够优化调用,它需要知道testsome 都是纯的,即为more(test()) 调用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());
}

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

不幸的是,目前还没有任何标准方法可以将函数声明为纯函数,以上是非标准的 GCC 扩展。有proposal N3744[[pure]] 添加到ISO C++(对纯度有更强的保证,some 在此下不需要是纯的)但我不知道它是否会成为C++ 17 或不是。

【讨论】:

  • 有谁知道如果没有提供属性,GCC 可以在多大程度上确定一个函数是否是纯函数?
  • "在这里,test 总是保证被准确地调用两次" False。大多数链接器都足够聪明,可以内联,此时它可以重新排列代码以调用 1 次甚至 0 次。
  • @Mooing Duck 你能说出这样的链接器吗?我用 GCC 和 Clang 测试了这段代码,也没有。
  • @StenSoft:我敢打赌两者都这样做。将test() 的正文更改为{return 3;},看看它被“调用”了多少次。
  • @MooingDuck 好的,我有经理让它像这样工作,呼叫优化为 1 个呼叫。 (即使在 -O3 上,默认情况下也不会启用 -flto,这就是我最初的测试没有捕捉到这一点的原因)很高兴学习新东西!
【解决方案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]

【讨论】:

  • 那么,你是说 GCC 试图证明一个函数是纯的,即使没有属性?你知道除了 lto 还需要开启什么优化选项吗?
  • @SU3 我可以通过两种方式看到它的发生。首先,GCC 可能会内联函数(-finline-functions),然后执行公共子表达式消除(-fgcse)。它也可以按照您的建议进行操作并自行正确标记该功能。我不熟悉,也找不到它现在执行此操作的机制,但我已经阅读过。
猜你喜欢
  • 1970-01-01
  • 2019-04-29
  • 2018-11-21
  • 2015-06-16
  • 1970-01-01
  • 1970-01-01
  • 2013-01-08
  • 1970-01-01
相关资源
最近更新 更多