Lazy Evaluation: Why is it faster, advantages vs disadvantages, mechanics (why it uses less cpu; examples?) and simple proof of concept examples
懒惰的评估被认为是一种将一个过程推迟到第一次需要它的时候的方法。这往往会避免重复的评估,这就是为什么我会想象它的执行速度会更快。像haskell(和javascript…)这样的功能语言内置此功能。
但是,我不理解其他"正常"方法(即相同的功能,但不使用延迟评估)的速度如何和为什么会变慢。这些其他方法如何以及为什么要进行重复评估?有人能通过给出简单的例子和解释每种方法的原理来详细阐述这一点吗?
另外,根据维基百科关于懒惰评估的页面,这些被认为是这种方法的优势:
但是,我们是否可以控制所需的计算,避免重复相同的计算?(1)我们可以使用链表来创建一个无限的数据结构(2)我们能不能已经……????我们可以定义类/模板/对象,并使用它们而不是原语(即javascript)。
此外,在我看来(至少从我所看到的情况来看),懒惰的评估使用递归和"head"和"tail"(连同其他概念)概念进行交互。当然,有些情况下递归是有用的,但是懒惰的评估是否比这更重要?不仅仅是解决问题的递归方法……streamjs是一个javascript库,它使用递归和一些其他简单操作(head、tail等)来执行延迟评估。
看来我没法改变主意…
提前感谢您的贡献。
我将在python 2.7和haskell中展示示例。好的。
比如说,你想对所有从0到10000000的数字做一个非常低效的求和。您可以使用Python中的for循环好的。
1 2 3 4 |
在我的电脑上,这需要大约1.3秒的时间来执行。相反,我把
1 2 3 4 5 6 | Mem usage Increment Line Contents =========================================== 10.875 MiB 0.004 MiB total = 0 165.926 MiB 155.051 MiB for i in range(10000000): 165.926 MiB 0.000 MiB total += i return total |
这说明在我们开始使用10.875MB之前,
1 2 3 4 5 6 | Mem usage Increment Line Contents =========================================== 11.000 MiB 0.004 MiB total = 0 11.109 MiB 0.109 MiB for i in xrange(10000000): 11.109 MiB 0.000 MiB total += i return total |
所以我们从11MB开始,
python有迭代器和生成器,当需要生成数据序列时(尽管没有任何东西阻止您将它们用于单个值),它们充当一种"懒惰"编程,但是haskell在语言中的每个值中都内置了懒惰,甚至是用户定义的值。这使您可以利用诸如数据结构之类的不适合内存的东西,而无需围绕这一事实编程复杂的方法。典型的例子是斐波那契序列:好的。
它非常优雅地表达了这个著名的序列,定义了一个生成所有斐波那契数的递归无限列表。它的CPU效率很高,因为所有的值都被缓存了,所以每个元素只需要计算一次(与简单的递归实现相比)1,但是如果计算的元素太多,您的计算机最终将耗尽RAM,因为您现在正在存储这个庞大的数字列表。这是一个例子,在这里,懒惰的编程让您有CPU效率,而不是RAM效率。不过,有一种方法可以解决这个问题。如果你要写好的。
然后,它在几乎恒定的内存中运行,并且运行得非常快,但是由于随后调用
这里有一个更复杂的例子,作者展示了如何在haskell中使用懒惰的编程和递归来用数组执行动态编程,这是一个最初认为非常困难并且需要突变的技巧,但是haskell使用"结"式递归很容易做到。它导致CPU和RAM的效率,并且比我在C/C++中所期望的更少。好的。
尽管如此,在很多情况下,懒惰的编程很烦人。通常情况下,你可以建立大量的thunk而不是按你的方式计算(我看你,
1)通过"幼稚的递归实现",我的意思是将斐波那契序列实现为好的。
通过这个实现,您可以非常清楚地看到数学定义,它非常类似于归纳证明的样式,先显示基本情况,然后显示一般情况。然而,如果我称之为
1 2 3 4 5 6 | fib 5 = fib 4 + fib 3 = fib 3 + fib 2 + fib 2 + fib 1 = fib 2 + fib 1 + fib 1 + fib 0 + fib 1 + fib 0 + fib 1 = fib 1 + fib 0 + fib 1 + fib 1 + fib 0 + fib 1 + fib 0 + fib 1 = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 8 |
当我们想分享这些计算时,这样,
通过在haskell中使用递归定义的列表,我们可以避免这种情况。在内部,这个列表是这样表示的:好的。
1 2 3 4 5 6 7 8 9 10 | fibs = 1 : 1 : zipWith (+) fibs (tail fibs) = 1 : 1 : zipWith (+) (f1:f2:fs) (f2:fs) ^--------------------^ ^ ^ ^-------------------|-------| = 1 : 1 : 2 : zipWith (+) (f2:f3:fs) (f3:fs) ^--------------------^ ^ ^ ^-------------------|-------| = 1 : 1 : 2 : 3 : zipWith (+) (f3:f4:fs) (f4:fs) ^--------------------^ ^ ^ ^-------------------|-------| |
所以希望您能在这里看到模式的形成,在构建列表时,它会保留指向最后两个元素的指针,以便计算下一个元素。这意味着对于计算出的第n个元素,执行了
另一方面,定义好的。
每次调用
一般来说,懒惰的评估不会更快。如果说延迟计算更有效,那是因为当你把lambda演算(当编译器完成对lambda演算后,haskell程序本质上就是这样的)看作是一个术语和约简规则系统,然后按照名称调用规则指定的顺序应用这些规则,并共享评估订单。Licy始终应用与按值调用计算指定的顺序遵循规则时相同或更少的缩减规则。
一般来说,这个理论结果并不能使延迟评估更快的原因是,转换为具有内存访问瓶颈的线性顺序机模型往往会使所有的减少执行的成本更高!最初在计算机上实现这个模型的尝试导致程序执行的速度比典型的热切地评估语言实现的速度慢得多。为了使Haskell的性能达到今天的水平,高效地实现懒惰的评估,需要大量的研究和工程技术。最快的haskell程序利用一种称为"严格性分析"的静态分析形式,这种分析形式试图在编译时确定总是需要哪些表达式,以便能够热切地对它们进行评估,而不是懒惰地进行评估。
在Haskell中,仍然有一些情况下,由于只评估结果所需的术语,简单的算法实现将更快地执行,但即使是热切的语言也总是有一些根据需要评估某些表达式的功能。条件表达式和短路布尔表达式是常见的例子,在许多热切的语言中,也可以通过将表达式包装在匿名函数或其他某种延迟形式中来延迟计算。因此,您通常可以使用这些机制(甚至更尴尬的重写)来避免评估那些在渴望的语言中不必要的昂贵事物。
哈斯凯尔的懒惰评估的真正优势不是与性能相关的。Haskell使得将表达式分开、以不同的方式重新组合它们变得更容易,并且通常会将代码解释为一个数学方程系统,而不是一组顺序评估的机器指令。通过不指定任何评估顺序,它强制语言的开发人员避免依赖简单的评估顺序的副作用,例如突变或IO。这反过来又导致了大量优雅的抽象,这些抽象通常是有用的,否则可能无法开发成可用性。
现在,haskell的状态是这样的,您可以编写高级、优雅的算法,使现有的高阶函数和数据结构比几乎任何其他高级类型语言都能更好地重用。一旦您熟悉了懒惰评估的成本和好处,以及如何控制它发生的时间,您就可以确保优雅的代码也能很好地执行。但是,将优雅的代码转换为高性能的状态并不一定是自动的,并且可能需要比使用类似但经过热切评估的语言进行更多的思考。
"懒惰评估"的概念只涉及一件事,而且只涉及这一件事:
The ability to postpone evaluation of something until needed
就是这样。
维基百科文章中的其他所有内容都是从它开始的。
无限的数据结构?不是问题。我们只是确保在您真正要求下一个元素之前,我们不会真正弄清楚它是什么。例如,如果要执行的操作只是将x增加1,那么询问一些代码x之后的下一个值是什么,将被填充。如果创建一个包含所有这些值的列表,它将填充计算机中可用的内存。如果你只知道下一个值是什么,而不是太多。
不必要的计算?当然。您可以返回一个包含许多属性的对象,当被询问时,这些属性将为您提供一些值。如果您不要求(即永远不要检查给定属性的值),那么计算出该属性值所必需的计算将永远不会完成。
控制流…?完全不确定这是怎么回事。
懒惰地评估某物的目的正是我所说的,从一开始就避免评估某物,直到你真正需要它为止。无论它是某个东西的下一个值,属性的值,不管怎样,添加对延迟评估的支持可能会节省CPU周期。
另一种选择是什么?
我想将一个对象返回到调用代码,其中包含任意数量的属性,其中一些属性可能计算起来很昂贵。如果没有延迟的评估,我将不得不计算所有这些属性的值:
对于懒惰的评估,你通常以2结尾。您可以推迟评估该属性的值,直到某些代码检查它。请注意,您可能会缓存一次评估后的值,这将在多次检查同一属性时节省CPU周期,但这是缓存,并不完全相同,而是在同一行工作中:优化。