4

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

哈斯克尔

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

4 回答 4

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 Int#s 生成工人。

然而,在这种情况下,生成快速代码不仅仅是在机器类型上操作。本机代码生成器以及 LLVM 后端生成的程序集基本上是将代码直接翻译为程序集,检查参数是 0 还是 1,如果不是,则调用两次函数并添加结果。两者都会产生一些我不理解的进入和退出代码,而在我的机器上,本机代码生成器会产生更快的代码。

对于 C 代码,clang -O3生成简单的程序集,减少繁琐并使用更多寄存器,

.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

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

没有任何优化的 gcc 使用一些间接寻址产生的结果基本相同,但比 GHC 使用 NCG 或 LLVM 后端产生的结果要少。同上-O1,但间接寻址更少。使用-O2,您将获得一个转换,因此程序集不会轻易映射回源,并且使用-O3, gcc 会产生相当惊人的效果

.LFB0:
    .cfi_startproc
    pushq   %r15
    .cfi_def_cfa_offset 16
    .cfi_offset 15, -16
    pushq   %r14
    .cfi_def_cfa_offset 24
    .cfi_offset 14, -24
    pushq   %r13
    .cfi_def_cfa_offset 32
    .cfi_offset 13, -32
    pushq   %r12
    .cfi_def_cfa_offset 40
    .cfi_offset 12, -40
    pushq   %rbp
    .cfi_def_cfa_offset 48
    .cfi_offset 6, -48
    pushq   %rbx
    .cfi_def_cfa_offset 56
    .cfi_offset 3, -56
    subq    $120, %rsp
    .cfi_def_cfa_offset 176
    testl   %edi, %edi
    movl    %edi, 64(%rsp)
    movq    $0, 16(%rsp)
    je      .L2
    cmpl    $1, %edi
    movq    $1, 16(%rsp)
    je      .L2
    movl    %edi, %eax
    movq    $0, 16(%rsp)
    subl    $1, %eax
    movl    %eax, 108(%rsp)
.L3:
    movl    108(%rsp), %eax
    movq    $0, 32(%rsp)
    testl   %eax, %eax
    movl    %eax, 72(%rsp)
    je      .L4
    cmpl    $1, %eax
    movq    $1, 32(%rsp)
    je      .L4
    movl    64(%rsp), %eax
    movq    $0, 32(%rsp)
    subl    $2, %eax
    movl    %eax, 104(%rsp)
.L5:
    movl    104(%rsp), %eax
    movq    $0, 24(%rsp)
    testl   %eax, %eax
    movl    %eax, 76(%rsp)
    je      .L6
    cmpl    $1, %eax
    movq    $1, 24(%rsp)
    je      .L6
    movl    72(%rsp), %eax
    movq    $0, 24(%rsp)
    subl    $2, %eax
    movl    %eax, 92(%rsp)
.L7:
    movl    92(%rsp), %eax
    movq    $0, 40(%rsp)
    testl   %eax, %eax
    movl    %eax, 84(%rsp)
    je      .L8
    cmpl    $1, %eax
    movq    $1, 40(%rsp)
    je      .L8
    movl    76(%rsp), %eax
    movq    $0, 40(%rsp)
    subl    $2, %eax
    movl    %eax, 68(%rsp)
.L9:
    movl    68(%rsp), %eax
    movq    $0, 48(%rsp)
    testl   %eax, %eax
    movl    %eax, 88(%rsp)
    je      .L10
    cmpl    $1, %eax
    movq    $1, 48(%rsp)
    je      .L10
    movl    84(%rsp), %eax
    movq    $0, 48(%rsp)
    subl    $2, %eax
    movl    %eax, 100(%rsp)
.L11:
    movl    100(%rsp), %eax
    movq    $0, 56(%rsp)
    testl   %eax, %eax
    movl    %eax, 96(%rsp)
    je      .L12
    cmpl    $1, %eax
    movq    $1, 56(%rsp)
    je      .L12
    movl    88(%rsp), %eax
    movq    $0, 56(%rsp)
    subl    $2, %eax
    movl    %eax, 80(%rsp)
.L13:
    movl    80(%rsp), %eax
    movq    $0, 8(%rsp)
    testl   %eax, %eax
    movl    %eax, 4(%rsp)
    je      .L14
    cmpl    $1, %eax
    movq    $1, 8(%rsp)
    je      .L14
    movl    96(%rsp), %r15d
    movq    $0, 8(%rsp)
    subl    $2, %r15d
.L15:
    xorl    %r14d, %r14d
    testl   %r15d, %r15d
    movl    %r15d, %r13d
    je      .L16
    cmpl    $1, %r15d
    movb    $1, %r14b
    je      .L16
    movl    4(%rsp), %r12d
    xorb    %r14b, %r14b
    subl    $2, %r12d
    .p2align 4,,10
    .p2align 3
.L17:
    xorl    %ebp, %ebp
    testl   %r12d, %r12d
    movl    %r12d, %ebx
    je      .L18
    cmpl    $1, %r12d
    movb    $1, %bpl
    je      .L18
    xorb    %bpl, %bpl
    jmp     .L20
    .p2align 4,,10
    .p2align 3
.L21:
    cmpl    $1, %ebx
    je      .L58
.L20:
    leal    -1(%rbx), %edi
    call    recfib
    addq    %rax, %rbp
    subl    $2, %ebx
    jne     .L21
.L18:
    addq    %rbp, %r14
    subl    $2, %r13d
    je      .L16
    subl    $2, %r12d
    cmpl    $1, %r13d
    jne     .L17
    addq    $1, %r14
.L16:
    addq    %r14, 8(%rsp)
    subl    $2, 4(%rsp)
    je      .L14
    subl    $2, %r15d
    cmpl    $1, 4(%rsp)
    jne     .L15
    addq    $1, 8(%rsp)
.L14:
    movq    8(%rsp), %rax
    addq    %rax, 56(%rsp)
    subl    $2, 96(%rsp)
    je      .L12
    subl    $2, 80(%rsp)
    cmpl    $1, 96(%rsp)
    jne     .L13
    addq    $1, 56(%rsp)
.L12:
    movq    56(%rsp), %rax
    addq    %rax, 48(%rsp)
    subl    $2, 88(%rsp)
    je      .L10
    subl    $2, 100(%rsp)
    cmpl    $1, 88(%rsp)
    jne     .L11
    addq    $1, 48(%rsp)
.L10:
    movq    48(%rsp), %rax
    addq    %rax, 40(%rsp)
    subl    $2, 84(%rsp)
    je      .L8
    subl    $2, 68(%rsp)
    cmpl    $1, 84(%rsp)
    jne     .L9
    addq    $1, 40(%rsp)
.L8:
    movq    40(%rsp), %rax
    addq    %rax, 24(%rsp)
    subl    $2, 76(%rsp)
    je      .L6
    subl    $2, 92(%rsp)
    cmpl    $1, 76(%rsp)
    jne     .L7
    addq    $1, 24(%rsp)
.L6:
    movq    24(%rsp), %rax
    addq    %rax, 32(%rsp)
    subl    $2, 72(%rsp)
    je      .L4
    subl    $2, 104(%rsp)
    cmpl    $1, 72(%rsp)
    jne     .L5
    addq    $1, 32(%rsp)
.L4:
    movq    32(%rsp), %rax
    addq    %rax, 16(%rsp)
    subl    $2, 64(%rsp)
    je      .L2
    subl    $2, 108(%rsp)
    cmpl    $1, 64(%rsp)
    jne     .L3
    addq    $1, 16(%rsp)
.L2:
    movq    16(%rsp), %rax
    addq    $120, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 56
    popq    %rbx
    .cfi_def_cfa_offset 48
    popq    %rbp
    .cfi_def_cfa_offset 40
    popq    %r12
    .cfi_def_cfa_offset 32
    popq    %r13
    .cfi_def_cfa_offset 24
    popq    %r14
    .cfi_def_cfa_offset 16
    popq    %r15
    .cfi_def_cfa_offset 8
    ret
    .p2align 4,,10
    .p2align 3
.L58:
    .cfi_restore_state
    addq    $1, %rbp
    jmp     .L18
    .cfi_endproc

这比其他任何测试都快得多。gcc 将算法展开到一个显着的深度,而 GHC 和 LLVM 都没有,这在这里产生了巨大的差异。

于 2012-11-04T23:07:29.263 回答
4

从使用更好的算法开始!

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

fib n = fibs !! n-1

fib 42会给你更快的答案。

使用更好的算法比进行细微的速度调整更重要

您可以使用这个定义(它的长度为 25801 位)愉快而快速地fib 123456在 ghci 中计算(即解释,甚至不编译)。您可能会让您的 C 代码计算得更快,但您将花费相当长的时间来编写它。这几乎没有花费我任何时间。我花了更多时间写这篇文章!

德:

  1. 使用正确的算法!
  2. Haskell 让你编写干净的代码版本,简单地记住答案。
  3. 有时,定义一个无限的答案列表并获取您想要的答案比编写一些更新值的循环版本更容易。
  4. 哈斯克尔很棒。
于 2012-11-04T22:51:19.853 回答
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尽管我很好奇您为什么不想使用更好的算法,但您也需要编译。

于 2012-11-04T22:54:01.500 回答
1

该功能已经非常严格。

函数严格的通常定义是,如果你给它未定义的输入,它本身就是未定义的。我从上下文假设您正在考虑不同的严格概念,即如果函数在产生结果之前评估其参数,则该函数是严格的。但通常检查一个值是否未定义的唯一方法是评估它,因此两者通常是等价的。

根据第一个定义,g肯定是严格的,因为它必须在知道要使用定义的哪个分支之前检查参数是否等于 0,所以如果参数未定义,g它会在尝试读取它时阻塞。

根据一个更非正式的定义,好吧,什么会g做错呢?前两个子句显然很好,这意味着当我们到达第三个子句时,我们必须已经评估了n。现在,在第三个子句中,我们增加了两个函数调用。更完整地说,我们有以下任务:

  1. 减去 1n
  2. 减去 2n
  3. 调用g结果为 1。
  4. 调用g结果为 2。
  5. 将 3. 和 4. 的结果相加。

懒惰可能会稍微扰乱这些操作的顺序,但是由于两者都+需要g它们的参数值才能运行它们的代码,所以实际上没有任何东西可以延迟任何大量的东西,当然编译器可以自由地运行这些操作严格的顺序,如果它只能表明它+是严格的(它是内置的,所以不应该太难)并且g是严格的(但它显然是)。所以任何合理的优化编译器都不会对此有太大的麻烦,而且任何非优化的编译器都不会因为做完全幼稚的事情而产生任何显着的开销(这当然不像 的情况foldl (+) 0 [1 .. 1000000])。

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

于 2012-11-05T12:23:25.467 回答