惰性评估如何以一种比产生的收益不需要更多开销的方式实施?

问题描述

我了解到有些地方懒惰的评估会阻止进行计算。例如,将两个数字相加然后将它们传递到一个函数参数中,该参数最终将永远不会被使用。

但是在我看来,存储和加载稍后要使用的操作会产生大量开销,而且这种开销往往会抵消任何收益。

有人可以解决这个问题吗?

解决方法

您是对的。惰性评估确实会产生大量开销,并且大多数时候您不会从中获得实际的性能提升。延迟评估的主要原因是它很方便-它使Haskell的语言语义更整洁,并且(例如)延迟/无限列表有时对于程序员很方便。

幸运的是,编译器通常可以在内部循环之外优化惰性机制,否则幼稚的实现会导致性能大幅下降。

,

出乎意料的是,事实证明,GHC的惰性评估实现不会在类似的严格运行时系统上引入可观的开销。创建用于延迟评估的thunk(即“存储”以后使用的操作)并最终强制其评估(即“加载”它)的表面上的“开销”取代了创建堆栈框架的严格评估“开销”。 ,调用函数并返回。结果,函数调用的成本会及时转移,但并没有显着增加。

的确,严格性(由程序员明确引入或由编译器自动识别)有时对于获得良好的性能是必需的,但这通常是因为严格性允许取消装箱和相关优化,或者在某些情况下避免了代价高昂的内存泄漏,导致过多的垃圾回收开销。惰性评估本身并不比严格评估昂贵得多。

this answer中,我对GHC RTS中的函数调用和典型的Java VM实现进行了较为详细的比较。答案集中在内存使用上(因为问题是关于垃圾回收的),但是很多讨论都更笼统地适用于性能。

如果要确定调用两个数字相乘的函数的开销,请汇总相关位:

bar :: Int -> Int -> Int
bar a b = a * b

由其他功能调用:

foo :: Int -> Int -> Int -> Int
foo x y z = let u = bar y z in x + u

然后在典型的严格实现中(例如Java JVM),字节代码可能类似于:

public static int bar(int,int);
  Code:
    stack=2,locals=2,args_size=2
       0: iload_0   // push a
       1: iload_1   // push b
       2: imul      // multiply and push result
       3: ireturn   // pop result and return it

public static int foo(int,int,locals=4,args_size=3
       0: iload_1   // push y
       1: iload_2   // push z
       2: invokestatic bar   // call bar,pushing result
       5: istore_3  // pop and save to "u"
       6: iload_0   // push x
       7: iload_3   // push u
       8: iadd      // add and push result
       9: ireturn   // pop result and return it

bar函数调用的开销(即,上面和如果插入bar之间的区别)看起来像是两次参数推送,调用本身和返回。

对于惰性版本,GHC(无优化)将这些代码编译为以下伪代码:

foo [x,y,z] =
    u = new THUNK(sat_u)                   // thunk,32 bytes on heap
    jump: (+) x u

sat_u [] =                                 // saturated closure for "bar y z"
    push UPDATE(sat_u)                     // update frame,16 bytes on stack
    jump: bar y z

bar [a,b] =
    jump: (*) a b

惰性bar函数调用的开销是在凹凸堆上创建一个thunk(与堆栈一样快),该thunk包含两个参数和一个指向sat_u的指针(还有返回空间)值,尽管没有“成本”,并且在(+)函数通过跳转到u来强制值sat_u时会出现“调用”(在上面的代码中不可见)。更新框架或多或少替换了返回值。 (在这种情况下,可以对其进行优化。)

最重要的是,至少在一个近似值上,GHC中实施的惰性评估与严格评估一样快,即使实际上已对所有评估进行评估。

,

之所以起作用,是因为编译器优化会在不需要时消除懒惰。

如果看到下一个计算将消耗前一个计算的结果,它只会生成严格的代码。

,

由于Haskell是纯语言,因此懒惰评估起着重要作用。这不仅仅是语言的一种功能,它使程序员可以以声明式的方式编写而不必担心计算顺序。

让我们举个例子。

`sum $ map (^2) [0..100]`

严格的语义在这里会发生什么。首先评估map函数。它将使用整个输入列表并生成(有分配,因为Haskell是纯净的)输出列表。然后sum将计算结果。

但是在惰性语义中,将不会构造此示例中的中间列表。因此,不会有不必要的内存工作。意味着减少垃圾收集器的工作。

因此,对于纯语言,惰性语义是避免构造中间对象的开销的一种方法。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...