首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >编译器如何将函数消除应用于不纯函数?

编译器如何将函数消除应用于不纯函数?
EN

Stack Overflow用户
提问于 2012-09-04 23:16:11
回答 1查看 417关注 0票数 7

在编写代码时,我经常会多次使用特定函数调用中的值。我意识到一个明显的优化就是在变量中捕获这些反复使用的值。这(伪码):

代码语言:javascript
复制
function add1(foo){ foo + 1; }
...
do_something(foo(1));
do_something_else(foo(1));

变成:

代码语言:javascript
复制
function add1(foo){ foo + 1; }
...
bar = foo(1);
do_something(bar);
do_something_else(bar);

然而,在我的经验中,这样做显式地降低了代码的可读性。我认为,如果我们选择的语言允许函数产生副作用,编译器就无法进行这种优化。

最近我研究了这个问题,如果我正确理解的话,这个优化就是/可以对函数必须是纯的语言进行优化。这并不令我感到惊讶,但据推测,这也可以用于不纯的功能。通过几个快速的Google搜索,我找到了以下代码片段:GCC 4.7改进

在执行前端优化时,即使对于不纯函数,也可以删除重复的函数调用。

编译器优化(维基百科)

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

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

这两段话给我提了两个问题。

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

最后:

  • 这种优化是否可以应用于任何语言,或者只有在满足某些条件时才适用?
  • 即使对于非常简单的函数,这种优化也是值得的吗?
  • 从堆栈存储和检索值会产生多少开销?

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

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2012-09-20 08:59:48

免责声明:我不是一个编译器/优化者,我只喜欢偷看生成的代码,并且喜欢阅读那些东西--所以这不是自动的。一个快速的搜索并没有出现太多关于功能的消除,所以它可能会做一些额外的魔术,这里没有解释。

优化器可以

  • 尝试内联函数调用(使用链接时间代码生成,这甚至可以跨编译单元工作)
  • 执行常见的子表达式消除,可能还有副作用重排序。

修改示例,并在C++中执行:

代码语言:javascript
复制
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));
}

解析为(伪码)

代码语言:javascript
复制
<register> = 4;
RW_A = register;
RW_A = register;

(注意:读取和写入易失性变量是“可观察的副作用”,优化器必须按照代码给出的顺序保持这种副作用。)

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

代码语言:javascript
复制
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;
}

生成以下伪码:

代码语言:javascript
复制
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的所有测试,默认的“发布”构建优化设置。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/12272563

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档