7

通常在编写代码时,我发现自己多次使用来自特定函数调用的值。我意识到一个明显的优化是在变量中捕获这些重复使用的值。这(伪代码):

function add1(foo){ foo + 1; }
...
do_something(foo(1));
do_something_else(foo(1));

变成:

function add1(foo){ foo + 1; }
...
bar = foo(1);
do_something(bar);
do_something_else(bar);

但是,根据我的经验,明确地这样做会使代码的可读性降低。我假设如果我们选择的语言允许函数具有副作用,编译器就无法进行这种优化。

最近我研究了这个,如果我理解正确的话,这种优化是/可以对函数必须是纯的语言进行。这并不让我感到惊讶,但据说这也可以用于不纯函数。通过一些快速的谷歌搜索,我发现了这些片段: GCC 4.7 Fortran 改进

在执行前端优化时,-faggressive-function-elimination 选项允许删除重复的函数调用,即使对于不纯函数也是如此。

编译器优化(维基百科)

例如,在某些语言中,函数不允许有副作用。因此,如果一个程序使用相同的参数多次调用同一个函数,编译器可以立即推断该函数的结果只需要计算一次。在允许函数具有副作用的语言中,另一种策略是可能的。优化器可以确定哪个函数没有副作用,并将此类优化限制为无副作用的函数。仅当优化器可以访问被调用函数时,才可能进行此优化。

根据我的理解,这意味着优化器可以确定函数何时是纯函数或非纯函数,并在函数为纯函数时执行此优化。我这样说是因为如果一个函数在给定相同的输入时总是产生相同的输出,并且没有副作用,那么它将满足这两个条件被认为是纯的。

这两个片段向我提出了两个问题。

  1. 如果函数不是纯函数,编译器如何能够安全地进行这种优化?(如-faggressive-function-elimination)
  2. 编译器如何确定一个函数是否是纯函数?(如维基百科文章中建议的策略)

最后:

  • 这种优化可以应用于任何语言,还是仅在满足某些条件时应用?
  • 即使对于极其简单的功能,这种优化是否值得?
  • 从堆栈中存储和检索值会产生多少开销?

如果这些是愚蠢或不合逻辑的问题,我深表歉意。它们只是我最近一直好奇的一些事情。:)

4

1 回答 1

2

免责声明:我不是编译器/优化器的人,我只是倾向于偷看生成的代码,并且喜欢阅读这些东西 - 所以这不是自动的。快速搜索并没有在 -faggressive-function-elimination 上找到太多,所以它可能会做一些这里没有解释的额外魔法。


优化器可以

  • 尝试内联函数调用(通过链接时间代码生成,这甚至可以跨编译单元)
  • 执行公共子表达式消除,并可能进行副作用重新排序。

稍微修改您的示例,并在 C++ 中进行:

extern volatile int RW_A = 0;  // see note below

int foo(int a)  { return a * a; }
void bar(int x) { RW_A = x; }

int _tmain(int argc, _TCHAR* argv[])
{
   bar(foo(2));
   bar(foo(2));
}

解析为(伪代码)

<register> = 4;
RW_A = register;
RW_A = register;

(注意:读取和写入 volatile 变量是“可观察到的副作用”,优化器必须按照代码给出的相同顺序保存。)


修改示例foo以产生副作用:

extern volatile int RW_A = 0;
extern volatile int RW_B = 0;
int accu = 1;

int foo(int a)  { accu *= 2; return a * a; }
void bar(int x) { RW_A = x; }

int _tmain(int argc, _TCHAR* argv[])
{
   bar(foo(2));
   bar(foo(2));

   RW_B = accu;
   return 0;
}

生成以下伪代码:

registerA = accu;
registerA += registerA;
accu = registerA;

registerA += registerA;
registerC = 4;
accu = registerA;

RW_A = registerC;
RW_A = registerC;

RW_B = registerA;

我们观察到公共子表达式消除仍然完成,并与副作用分开。内联和重新排序允许将副作用与“纯”部分分开。

请注意,编译器读取并急切地写回accu,这不是必需的。我不确定这里的理由。


总结:

编译器不需要测试纯度。它可以识别需要保留的副作用,然后根据自己的喜好转换其余的。

这种优化是值得的,即使对于微不足道的函数,因为除其他外,

  • CPU和内存是共享资源,你用的可能会从别人那里拿走
  • 电池寿命
  • 次要优化可能是通向较大优化的途径

堆栈内存访问的开销通常约为 1 个周期,因为堆栈顶部通常已经在 1 级缓存中。请注意,通常应该用粗体表示:它可能“更好”,因为读/写可能会被优化掉,或者它可能更糟,因为 L1 缓存上增加的压力会将一些其他重要数据刷新回 L2。

极限在哪里?

理论上,编译时间。在实践中,优化器的可预测性和正确性是额外的权衡。


使用 VC2008 进行的所有测试,“Release”构建的默认优化设置。

于 2012-09-20T08:59:48.373 回答