4

(fib)の評価をg完全に厳しくする方法を教えてください。(この指数解は最適ではないことを知っています。その再帰を完全に厳密にする方法を知りたいです/可能であれば/)

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

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の厳密性アナライザーはかなり優れており、ここのような単純なケースでは、ポリモーフィズムが含まれず、関数がそれほど複雑ではない場合、すべての値をボックス化解除して、ボックス化されていないsを使用してワーカーを生成できることがわかりますInt#

ただし、このような場合は、マシンタイプを操作するだけでなく、高速コードを生成することもできます。ネイティブコードジェネレーターとLLVMバックエンドによって生成されるアセンブリは、基本的にコードをアセンブリに直接変換し、引数が0か1かを確認し、そうでない場合は関数を2回呼び出して結果を追加します。どちらも、私が理解できないいくつかの開始コードと終了コードを生成します。私のボックスでは、ネイティブコードジェネレーターがより高速なコードを生成します。

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あなたにはるかに速く答えを与えるでしょう。

速度を微調整するよりも、優れたアルゴリズムを使用することがはるかに重要です。

fib 123456この定義(長さ25801桁)を使用して、ghciで(つまり、解釈され、コンパイルされていない)楽しく迅速に計算できます。あなたはそれをより速く計算するためにあなたのCコードを得るかもしれません、しかしあなたはそれを書くのにかなりの時間がかかります。これにはほとんど時間がかかりませんでした。私はこの投稿を書くのにもっと多くの時間を費やしました!

道徳:

  1. 適切なアルゴリズムを使用してください!
  2. Haskellを使用すると、答えを簡単にメモして、クリーンなバージョンのコードを記述できます。
  3. 値を更新するループバージョンを作成するよりも、回答の無限のリストを定義して必要なリストを取得する方が簡単な場合があります。
  4. Haskellは素晴らしいです。
于 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

機能はすでに完全に厳密です。

厳密である関数の通常の定義は、未定義の入力を与えると、それ自体が未定義になるというものです。コンテキストから、厳密性の別の概念を考えていると思います。つまり、関数が結果を生成する前に引数を評価する場合、関数は厳密です。ただし、通常、値が未定義かどうかを確認する唯一の方法は値を評価することであるため、2つは同等であることがよくあります。

最初の定義によると、定義gのどのブランチを使用するかを知る前に引数がゼロに等しいかどうかをチェックする必要があるため、確かに厳密です。したがって、引数が定義されていない場合、引数gを読み取ろうとするとそれ自体がチョークします。

より非公式な定義によると、まあ、何がg間違っている可能性がありますか?最初の2つの句は明らかに問題ありません。つまり、3番目の句に到達するまでに、はすでに評価されている必要がありnます。ここで、3番目の句に、2つの関数呼び出しが追加されています。より完全には、次のタスクがあります。

  1. から1を引くn
  2. から2を引くn
  3. g1の結果で呼び出します。
  4. g2の結果で呼び出します。
  5. 3.と4.の結果を足し合わせます。

怠惰はこれらの操作の順序を少し混乱させる可能性がありますが、両方ともコードを実行する前に引数の値を必要+gするため、実際には大幅な遅延はなく、コンパイラはこれらの操作を自由に実行できます厳密で+ある(組み込みであるため、それほど難しくない)こととg厳密である(ただし、明らかにそうである)ことだけを示すことができる場合は、厳密な順序。したがって、合理的な最適化コンパイラーはこれに関してそれほど問題を抱えることはなく、さらに、最適化されていないコンパイラーはfoldl (+) 0 [1 .. 1000000]、完全に単純なことを行うことによって大きなオーバーヘッドを被ることはありません(確かにの状況とは異なります)。

教訓は、関数がその引数を何かとすぐに比較するとき、その関数はすでに厳密であり、適切なコンパイラーはその事実を利用して、通常の怠惰のオーバーヘッドを排除できるということです。これは、ランタイムシステムの起動にかかる時間など、HaskellプログラムをCプログラムよりも少し遅くする傾向がある他のオーバーヘッドを排除できるという意味ではありません。パフォーマンスの数値だけを見ている場合は、プログラムが厳密であるか怠惰であるかよりも多くのことが起こっています。

于 2012-11-05T12:23:25.467 に答える