关于Haskell性能的推理

下面两个计算Fibonacci序列的第n项的Haskell程序具有非常不同的性能特征:

fib1 n =
  case n of
    0 -> 1
    1 -> 1
    x -> (fib1 (x-1)) + (fib1 (x-2))

fib2 n = fibArr !! n where
  fibArr = 1:1:[a + b | (a, b) <- zip fibArr (tail fibArr)]

它们非常接近数学上的相同,但是fib2使用列表符号来记忆它的中间结果,而fib1具有明确的递归。 尽管中间结果可能被缓存在fib1 ,但即使对于fib1 25 ,执行时间也会成为问题,这表明递归步骤总是被评估。 参考透明度是否有助于Haskell的表现? 如何提前知道是否会或不会?

这只是我担心的事情的一个例子。 我想听听关于如何克服懒惰执行的函数式编程语言性能的推理困难的想法。


总结:我接受3lectrologos的回答,因为关于语言性能的问题和编译器的优化没有多大关系,这点似乎在Haskell中非常重要 - 比我熟悉的任何其他语言都更重要用。 我倾向于说,编译器的重要性是区分对懒惰功能语言的性能推理与其他任何类型的性能的推理的因素。


附录:关于这个问题的任何人都可能想看看Johan Tibell关于高性能Haskell的演讲的幻灯片。


在你特定的斐波那契示例中,不难看出为什么第二个应该跑的更快(尽管你没有指定f2是什么)。

这主要是一个算法问题:

  • fib1实现了纯粹的递归算法,并且(据我所知)Haskell没有“隐式记忆”的机制。
  • fib2使用明确的memoization(使用fibArr列表来存储以前计算的值。
  • 一般来说,对像Haskell这样的懒惰语言进行性能假设要比对渴望的语言进行性能假设要困难得多。 不过,如果你了解潜在的机制 (特别是懒惰)并收集一些经验 ,你将能够对性能做出一些“预测”。

    参照透明度 (至少)有两种方式提高(可能)绩效:

  • 首先,你(作为一名程序员)可以肯定,对同一个函数的两个调用总是返回相同的,所以你可以在不同的情况下利用这个来获得性能。
  • 其次(也是更重要的一点),Haskell编译器可以肯定上述事实,并且这可能会启用许多不纯的语言中无法启用的优化(如果您曾经编写过编译器或有编译器优化的经验,可能意识到这一点的重要性)。
  • 如果你想了解更多关于Haskell设计选择(懒惰,纯粹)背后的推理,我建议阅读它。


    关于性能的推理在Haskell和一般懒惰语言中通常很难,尽管并非不可能。 一些技术都属于克里斯·奥卡萨基的纯粹功能的数据结构(也可在网上在以前的版本)。

    确保性能的另一种方法是使用注释或延续传递样式来修正评估顺序。 这样你可以控制什么时候评估。

    在你的例子中,你可能会计算数字“自下而上”,并将前两个数字传递给每个迭代:

    fib n = fib_iter(1,1,n)
        where
          fib_iter(a,b,0) = a
          fib_iter(a,b,1) = a
          fib_iter(a,b,n) = fib_iter(a+b,a,n-1)
    

    这导致线性时间算法。

    无论何时您有一个动态编程算法,其中每个结果都依赖于前面的N个结果,您可以使用这种技术。 否则,你可能不得不使用一个数组或完全不同的东西。


    您的fib2实现使用memoization,但每次调用fib2时,都会重建“整个”结果。 打开ghci时间和大小分析:

    Prelude> :set +s
    

    如果它正在执行“之间”调用,后续调用将更快并且不使用内存。 拨打fib2 20000两次,看看自己。

    通过比较,您可以定义精确的数学标识,

    -- the infinite list of all fibs numbers.
    fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
    
    memoFib n = fibs !! n
    

    实际上确实使用记忆,如你所见。 如果你运行memoFib 20000两次,你会看到第一次使用的时间和空间,然后第二次调用是瞬间的,并且不记忆。 没有魔法,也没有隐含的记忆,就像评论可能暗示的那样。

    现在谈谈您的原始问题:在Haskell中优化和推理性能...

    我不会称自己是Haskell的专家,我只用了3年,其中2次在我的工作场所,但我不得不优化并了解如何对其性能进行某种程度的推理。

    正如其他职位懒惰所提到的是你的朋友,可以帮助你获得表现,但你必须控制什么是懒惰评估和什么是严格评估。

    检查foldl与foldr的比较

    foldl实际上存储了“如何”计算值,即它是懒惰的。 在某些情况下,你可以节省懒惰的时间和空间,就像“无限”的纤维。 无限的“纤维”不会产生所有这些,但知道如何。 当你知道你需要的价值,你可能只是得到它“严格”说...这就是严格的注释是有用的,让你回到控制。

    我记得很多次,在lisp中你必须“尽量减少”收购。

    了解什么是严格评估,以及如何强制这一点很重要,但了解你对记忆做了多少“捣乱”。 记住Haskell是不可变的,这意味着更新一个“变量”实际上是创建一个修改副本。 因为(:)不会与(++)相反地复制内存,所以预先用(:)比用(++)追加效率高得多。 无论何时更新大的原子块(即使是单个字符),都需要复制整个块来表示“更新”版本。 您构建数据和更新数据的方式可能会对性能产生重大影响。 ghc分析器是你的朋友,并会帮助你发现这些。 确定垃圾收集器速度很快,但没有做任何事情更快!

    干杯

    链接地址: http://www.djcxy.com/p/80801.html

    上一篇: Reasoning about performance in Haskell

    下一篇: Bootstrapping a compiler: why?