我刚刚了解了惰性评估的工作原理,我想知道:为什么惰性评估没有应用于当前生产的每个软件中?为什么仍使用急切评估?
我刚刚了解了惰性评估的工作原理,我想知道:为什么惰性评估没有应用于当前生产的每个软件中?为什么仍使用急切评估?
Answers:
懒惰评估需要记账本钱-您必须知道它是否已经评估过,诸如此类。总是会评估急切的评估,因此您不必知道。在并发上下文中尤其如此。
其次,它的琐碎通过打包成一个函数对象急于评价转化为懒惰评估稍后调用,如果你愿意的话。
第三,懒惰的评估意味着失去控制。如果我懒惰地评估从磁盘读取文件怎么办?还是抽时间?那是不可接受的。
急切的评估可以更有效,更可控,并且可以轻松地转换为惰性评估。为什么要进行懒惰评估?
readFile
是正是我所需要的。此外,从懒惰的评估转换为热切的评估也是微不足道的。
head [1 ..]
用一种热切评估的纯语言会给您带来1
什么,因为在Haskell中它能给您带来什么?
主要是因为懒惰的代码和状态可能混合得很糟,并导致一些难以发现的错误。如果从属对象的状态发生变化,则在评估时,惰性对象的值可能是错误的。最好让程序员在知道合适的情况下将对象显式地编写为懒惰对象。
另外,Haskell对所有内容都使用了惰性评估。这是可能的,因为它是一种功能性语言,并且不使用状态(除非在一些特殊情况下清楚地标记了它们)
set!
在惰性方案解释器中使用。> :(
惰性评估并不总是更好。
惰性评估的性能优势可能很大,但是在急切的环境中避免大多数不必要的评估并非难事-惰性使得它容易且完整,但是在代码中不必要的评估很少是主要问题。
惰性评估的好处在于,它可以让您编写更清晰的代码。通过过滤无限自然数列表获得第10个素数并采用该列表的第10个元素是进行过程中最简洁明了的方法之一:(伪代码)
let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)
我相信,如果没有懒惰,那么简洁地表达事情将非常困难。
但是懒惰并不是一切的答案。首先,懒惰不能在状态存在时透明地应用,并且我相信不能自动检测到状态(除非您在状态非常明确的情况下使用Haskell进行工作)。因此,在大多数语言中,懒惰需要手动完成,这使事情变得不太清楚,从而消除了懒惰评估的一大好处。
此外,懒惰在性能上有缺陷,因为它会导致保持未求值的表达式的开销很大。它们会耗尽存储空间,并且比简单的值要慢。发现您必须热切地编写代码是很平常的,因为懒惰的版本运行缓慢,并且有时很难对性能进行推理。
随着它的发生,没有绝对的最佳策略。如果您可以利用无限的数据结构或它允许使用的其他策略编写更好的代码,那么懒惰是很棒的选择,但是渴望可以更轻松地进行优化。
这是对渴望和懒惰的评估的利弊的简短比较:
渴望评估:
不必要地评估内容的潜在开销。
不受阻碍,快速评估。
惰性评估:
没有不必要的评估。
每次使用值时的簿记开销。
因此,如果您有许多不必求值的表达式,则懒惰会更好;但是,如果您从来没有一个不需要计算的表达式,那么惰性就是纯粹的开销。
现在,让我们来看看真实的世界软件:有多少的功能,你写你不能要求他们所有的论据评价?特别是对于仅做一件事的现代短函数而言,属于此类的函数百分比非常低。因此,懒惰的评估在大多数情况下只会引入簿记开销,而没有机会真正节省任何费用。
因此,懒惰的评估根本无法平均地付出代价,热切的评估更适合现代代码。
正如@DeadMG指出的那样,惰性评估需要簿记开销。相对于急切的评估,这可能是昂贵的。考虑以下语句:
i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3
这将需要一些计算才能计算出来。如果我使用惰性评估,则每次使用时都需要检查它是否已经评估过。如果这是在频繁使用的紧密循环内,则开销会显着增加,但没有任何好处。
有了急切的评估和良好的编译器,您可以在编译时计算公式。如果合适,大多数优化器会将分配移出发生的任何循环。
惰性评估最适合加载不经常访问的数据,并且检索开销很大。因此,与核心功能相比,它更适合边缘情况。
通常,最好的做法是尽早评估经常访问的内容。惰性评估不适用于这种做法。如果您将始终访问某些内容,那么所有惰性评估都将增加开销。随着要访问的项目变得不太可能被访问,使用惰性评估的成本/收益降低。
始终使用惰性评估还意味着尽早优化。这是一种不好的做法,通常会导致代码变得更加复杂和昂贵,否则情况可能会如此。不幸的是,过早的优化通常会使代码的执行速度比简单的代码慢。在无法衡量优化效果之前,优化代码是一个坏主意。
避免过早的优化不会与良好的编码习惯冲突。如果未应用良好实践,则初始优化可能包括应用良好的编码实践,例如将计算移出循环。
如果我们可能必须完全评估表达式来确定其值,那么延迟评估可能是不利的。假设我们有很长的布尔值列表,并且我们想找出它们是否全部为真:
[True, True, True, ... False]
为了做到这一点,无论如何,我们都必须查看列表中的每个元素,因此不可能延迟评估。我们可以使用折叠来确定列表中的所有布尔值是否为真。如果我们使用了使用惰性求值的对折权,那么我们就无法获得惰性求值的任何好处,因为我们必须查看列表中的每个元素:
foldr (&&) True [True, True, True, ... False]
> 0.27 secs
在这种情况下,向右折叠比不向左折叠的严格折叠要慢得多,后者不使用惰性计算:
foldl' (&&) True [True, True, True, ... False]
> 0.09 secs
原因是使用尾部递归严格折左,这意味着它会累加返回值,并且不会建立并在内存中存储大量操作链。这比懒惰折叠要快得多,因为这两个函数无论如何都必须查看整个列表,并且折叠右不能使用尾部递归。因此,重点是,您应该使用最适合手头任务的方法。