6

在 C 中,如果我有一个看起来像的函数调用

// main.c
...
do_work_on_object(object, arg1, arg2);
...

// object.c
void do_work_on_object(struct object_t *object, int arg1, int arg2)
{
  if(object == NULL)
  {
    return;
  }
  // do lots of work
}

然后编译器将在 main.o 中生成很多东西来保存状态、传递参数(希望在这种情况下在寄存器中)和恢复状态。

但是,在链接时可以观察到 arg1 和 arg2 没有用于快速返回路径,因此可以将清理和状态恢复短路。链接器是否倾向于自动执行此类操作,或者是否需要打开链接时优化 (LTO) 才能使此类操作正常工作?

(是的,我可以检查反汇编代码,但我对编译器和链接器的一般行为以及多种架构感兴趣,因此希望从其他人的经验中学习。)

假设分析表明这个函数调用值得优化,我们是否应该期望下面的代码明显更快(例如,不需要使用 LTO)?

// main.c
...
if(object != NULL)
{
  do_work_on_object(object, arg1, arg2);
}
...

// object.c
void do_work_on_object(struct object_t *object, int arg1, int arg2)
{
  assert(object != NULL) // generates no code in release build
  // do lots of work
}
4

2 回答 2

3

一些编译器(如 GCC 和 clang)能够进行“ shrink-wrap ”优化,以延迟保存调用保留的 reg 直到可能的提前退出,如果他们能够发现该模式。但有些没有,例如显然 MSVC 16.11 仍然没有

我不认为任何人只对调用者的提前检查进行部分内联,以避免传递参数和调用/调用本身的开销。


由于编译器/链接器对此的支持并不普遍,即使对于收缩包装也不总是成功的,因此您可以以一种获得很多好处的方式编写代码,但代价是将函数的逻辑分成两个地方。

如果您有一个几乎不需要任何代码但经常发生的快速路径,那么将该部分放在标题中以便它被内联,并回退到调用函数的其余部分(您将其设为私有,因此它可以假设内联部分中的任何检查都已完成)。

例如,当 galois16 因子为零时,处理数据块的 par2 例程具有快速路径。(dst[i] += 0 * src[i]即使*在 Galois16 中是乘法,也是无操作,并且+=是 GF16 加法(即按位异或))。

请注意有问题的提交如何将旧函数重命名为InternalProcess,并添加一个新函数来template<class g> inline bool ReedSolomon<g>::Process检查快速路径,否则调用InternalProcess. (以及进行一堆不相关的空白更改,还有一些ifdefs……它最初是 2006 年的 CVS 提交。)

提交中的评论声称修复的整体速度提高了 8%。

于 2015-07-07T21:31:05.007 回答
0

设置或清理状态代码都不能短路,因为编译后的代码是静态的,它不知道程序执行时会发生什么。所以编译器总是必须设置整个参数堆栈。

考虑两种情况:一种objectnil,另一种不是。汇编代码如何知道是否将其余参数放入堆栈?尤其是调用者负责将参数放置在适当的位置(堆栈或注册表)。

于 2015-04-17T19:11:48.367 回答