C vs Haskell中的朴素斐波那契

时间:2012-11-04 22:42:53

标签: haskell ghc

请问,如何对g(fib)的评估完全严格? (我知道这个指数解决方案不是最优的。我想知道如何使递归完全严格 /如果可能/)

的Haskell

g :: Int -> Int
g 0 = 0
g 1 = 1
g x = g(x-1) + g(x-2)
main = print $ g 42

因此它的运行速度与天真的C解决方案一样快:

C

#include <stdio.h>

long f(int x)
{
    if (x == 0) return 0;
    if (x == 1) return 1;
    return f(x-1) + f(x-2);
}

int main(void)
{
    printf("%ld\n", f(42));
    return 0;
}

注意:此fibs-recursion仅用作超简单示例。我完全知道,有很多更好的算法。但肯定有递归算法,它们没有那么简单和有效的替代方案。

4 个答案:

答案 0 :(得分:16)

答案是,GHC自己完全严格评估(当你通过优化编译给它机会时)。原始代码生成核心

Rec {
Main.$wg [Occ=LoopBreaker] :: GHC.Prim.Int# -> GHC.Prim.Int#
[GblId, Arity=1, Caf=NoCafRefs, Str=DmdType L]
Main.$wg =
  \ (ww_s1JE :: GHC.Prim.Int#) ->
    case ww_s1JE of ds_XsI {
      __DEFAULT ->
        case Main.$wg (GHC.Prim.-# ds_XsI 1) of ww1_s1JI { __DEFAULT ->
        case Main.$wg (GHC.Prim.-# ds_XsI 2) of ww2_X1K4 { __DEFAULT ->
        GHC.Prim.+# ww1_s1JI ww2_X1K4
        }
        };
      0 -> 0;
      1 -> 1
    }
end Rec }

正如您所知,如果您了解GHC的核心,则完全严格并使用未装箱的原始机器整数。

(不幸的是,机器代码gcc从C源产生的速度更快。)

GHC的严格性分析器相当不错,在像这里这样简单的情况下,没有涉及多态性且函数不太复杂,你可以依靠它发现它可以取消包装所有值以使用unboxed生成一个worker {{ 1}} S上。

但是,在这种情况下,生产快速代码不仅仅是在机器类型上运行。由本机代码生成器以及LLVM后端生成的程序集基本上是代码到程序集的直接转换,检查参数是0还是1,如果没有调用该函数两次并添加结果。两者都产生一些我不理解的入口和出口代码,在我的盒子上,本机代码生成器生成更快的代码。

对于C代码,Int#生成简单的程序集,减少错误并使用更多寄存器,

clang -O3

(由于某些原因,我今天的系统表现比昨天好得多)。从Haskell源和C生成的代码之间的性能差异来自后者使用寄存器,其中前者使用间接寻址,算法的核心在两者中都是相同的。

gcc,没有任何优化,使用一些间接寻址产生的结果基本相同,但比使用NCG或LLVM后端产生的GHC少。使用.Ltmp8: .cfi_offset %r14, -24 movl %edi, %ebx xorl %eax, %eax testl %ebx, %ebx je .LBB0_4 # BB#1: cmpl $1, %ebx jne .LBB0_3 # BB#2: movl $1, %eax jmp .LBB0_4 .LBB0_3: leal -1(%rbx), %edi callq recfib movq %rax, %r14 addl $-2, %ebx movl %ebx, %edi callq recfib addq %r14, %rax .LBB0_4: popq %rbx popq %r14 popq %rbp ret ,同上,但间接寻址更少。使用-O1,您将获得转换,以便程序集不会轻易地映射回源,并且使用-O2,gcc会产生相当惊人的

-O3

比其他任何测试都要快得多。 gcc将算法展开到一个非常深的范围,GHC和LLVM都没有这样做,这在这里产生了巨大的差异。

答案 1 :(得分:4)

首先使用更好的算法!

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

fib n = fibs !! n-1

fib 42会更快地给你答案。

使用更好的算法比使用轻微的速度调整更重要

你可以用这个定义快乐地快速计算ghci中的fib 123456(即解释,甚至不编译)(长度为25801位)。你可能会得到你的C代码来更快地计算,但你需要花一些时间来编写它。这几乎没有任何时间。我花了很多时间写这篇文章!

道德:

  1. 使用正确的算法!
  2. Haskell让你编写干净的代码版本,简单地回忆起答案。
  3. 有时更容易定义无限的答案列表并抓住你想要的答案,而不是写一些更新值的循环版本。
  4. Haskell太棒了。

答案 2 :(得分:3)

这是完全严格的。

g :: Int -> Int
g 0 = 0
g 1 = 1
g x = a `seq` b `seq` a + b where
   a = g $! x-1
   b = g $! x-2
main = print $! g 42

$!$(低优先级函数应用程序)相同,只是它在函数参数中是严格的。

您也希望使用-O2进行编译,但我很好奇为什么您不想使用更好的算法。

答案 3 :(得分:1)

该功能已经完全严格。

一个严格的函数的通常定义是,如果你给它未定义的输入,它本身将是未定义的。我假设你从上下文中想到了一个不同的严格概念,即如果函数在生成结果之前计算其参数,则它是严格的。但通常检查值是否未定义的唯一方法是对其进行评估,因此两者通常是等价的。

根据第一个定义,g当然是严格的,因为它必须在知道要使用的定义的哪个分支之前检查参数是否等于零,因此如果参数未定义,{{1}当它试图读取它时,它本身就会窒息。

根据一个更为非正式的定义,那么g可能做错了什么?前两个条款显然很好,并且意味着当我们到达第三个条款时,我们必须已经评估过g。现在,在第三个子句中,我们增加了两个函数调用。更完整的是,我们有以下任务:

  1. n
  2. 中减去1
  3. n
  4. 中减去2
  5. 调用n,结果为1。
  6. 调用g,结果为2。
  7. 一起添加3.和4.的结果。
  8. 懒惰可以稍微搞乱这些操作的顺序,但由于g+在他们可以运行他们的代码之前需要他们的参数值,所以实际上没有什么可以被任何重要的延迟如果它只能显示g是严格的(它是内置的,因此不应该太难),那么编译器可以自由地按严格的顺序运行这些操作。+是严格(但显然是)。所以任何合理的优化编译器都不会有太多的麻烦,而且任何非优化编译器都不会产生任何显着的开销(它肯定不像g的情况)通过做完全天真的事情。

    经验教训是,当一个函数立即将其参数与某些东西进行比较时,该函数已经是严格的,并且任何体面的编译器都能够利用这个事实来消除懒惰的通常开销。这并不意味着它将能够消除其他开销,例如启动运行时系统所花费的时间,这往往会使Haskell程序比C程序慢一点。如果您只是在查看性能数据,那么除了您的程序是严格还是懒惰之外,还有更多的内容。