14

この質問に出くわしました 、フィボナッチ数の計算に関するさまざまなコンパイラのパフォーマンスを素朴な方法で比較しました。

Haskell でこれを実行して、C と比較してみました。

C コード:

#include <stdio.h>
#include <stdlib.h>

int fib (int n) {
  if (n < 2) return 1;
  return fib (n-1) + fib (n-2);
}

int main (int argc, char* argv[]) {
  printf ("%i\n", fib (atoi(argv[1])));
  return 0;
}

結果:

> gcc -O3 main.c -o fib
> time ./fib 40
165580141
real    0m0.421s
user    0m0.420s
sys 0m0.000s

ハスケル:

module Main where
import System.Environment (getArgs)

fib :: Int -> Int
fib n | n < 2 = 1
      | otherwise = fib (n-1) + fib (n-2)

main = getArgs >>= print . fib . read . head

結果:

> ghc -O3 -fllvm -optlo-O3 Main.hs -o fib
> time ./fib 40
165580141
real    0m1.476s
user    0m1.476s
sys 0m0.000s

プロファイリング

> ghc -O3 -fllvm -optlo-O3 -prof -auto-all -caf-all -rtsopts Main.hs -fforce-recomp -o fib
> ./fib 40 +RTS -prof

fib100% の時間と割り当てが必要であることがわかりますが、当然のことです。ヒープのプロファイルをいくつか取りましたが、それらが何を意味するのかわかりません:

> ./fib 40 +RTS -hc

ここに画像の説明を入力

> ./fib 40 +RTS -hd

ここに画像の説明を入力

私の質問: この Haskell プログラムのパフォーマンスを C に近づけるために、私の側からできることはありますか? それとも、このマイクロベンチマークで GHC がたまたま速度を低下させる方法なのですか? (フィブを計算するための漸近的に高速なアルゴリズムを求めているわけではありません。)

どうもありがとうございました。

[編集]

ghc -O3この場合よりも高速であることが判明しましたghc -O3 -fllvm -optlo-O3。しかしoptlo-block-placement、LLVM バックエンドには明らかな違いがありました。

> ghc -O3 Main.hs -o fib -fforce-recomp
> time ./fib 40
165580141
real    0m1.283s
user    0m1.284s
sys 0m0.000s

> ghc -O3 -fllvm -optlo-O3 -o fib -fforce-recomp
> time ./fib 40
165580141
real    0m1.449s
user    0m1.448s
sys 0m0.000s

> ghc -O3 -fllvm -optlo-O3 -optlo-block-placement -o fib -fforce-recomp
> time ./fib 40
165580141
real    0m1.112s
user    0m1.096s
sys 0m0.016s

私がこれを調査したかった理由は、このプログラムでは C と OCaml の両方が Haskell よりも大幅に高速だったからです。私はそれを受け入れることができず、できる限りのことをすでに行っていることを確認するためにもっと学びたいと思っていました:D

> ocamlopt main.ml -o fib
> time ./fib 40
165580141
real    0m0.668s
user    0m0.660s
sys 0m0.008s
4

4 に答える 4

9

fibGHCはスタック上でのみ動作する関数にコンパイルされるため、ここではヒープ プロファイルはあまり興味深いものではありません。mainプロファイルを見てください...これまでに割り当てられたのは800バイトだけで、実装のわずかなオーバーヘッドです。

GHC のコア レベルに関する限り、これは実際に可能な限り最適化されます。ただし、低レベルのコード生成は別の問題です。GHC が生成するコードを簡単に見てみましょう。

_Main_zdwfib_info:
.Lc1RK:
    leal -8(%ebp),%eax
    cmpl 84(%ebx),%eax
    jb .Lc1RM

これは、スタック スペースのチェックです。オペレーティングシステムがスタックスペースの割り当てを処理できるようにするため、おそらくCは必要ありません。Haskell にはユーザー レベルのスレッドがあるため、スタック領域は手動で管理されます。

    cmpl $2,0(%ebp)
    jl .Lc1RO

コードの 2 との比較。

    movl 0(%ebp),%eax
    decl %eax

パラメーターはスタックから再ロードされ、再帰呼び出しのパラメーターを取得するためにデクリメントされます。リロードはおそらく不要ですが、違いがあるかどうかはわかりません。

    movl %eax,-8(%ebp)
    movl $_s1Qp_info,-4(%ebp)
    addl $-8,%ebp
    jmp _Main_zdwfib_info

パラメータと戻りアドレスはスタックの一番上にプッシュされ、再帰するためにラベルに直接ジャンプします。

.Lc1RO:
    movl $1,%esi
    addl $4,%ebp
    jmp *0(%ebp)

パラメータが 2 未満の場合のコード。戻り値はレジスタに渡されます。

結論: すべてが正常に機能しているように見えますが、プログラムを変更することで、これをさらに引き出すことはできそうにありません。カスタムスタックチェックは速度低下の明らかな原因ですが、完全な時間差のせいにすることができるかどうかはわかりません.

于 2011-07-16T11:07:04.657 に答える
6

これらは、言うように本当に弱い「ベンチマーク」のようbarsoapです。次のほとんど同じように単純なプログラムを比較するとします。

module Main where
import System.Environment (getArgs)

fib ::  Int ->  Int
fib 0 = 1
fib 1 = 1
fib 2 = 2 
fib n = (\x y -> x + y + y )  (fib (n-3))  (fib (n-2) )

main = getArgs >>= print . fib . read . head  

...そしてもう一方の隅に...

#include <stdio.h>
#include <stdlib.h>

int fib (int n) {
  if (n < 2) return 1;
  if (n < 3) return n;
  return (fib (n-3) + fib (n-2)) + fib (n-2);
}

int main (int argc, char* argv[]) {
  printf ("%i\n", fib (atoi(argv[1])));
  return 0;
}

その後、グロリアスghcは を押しつぶしgccますが、それほど驚くことではありません。

$ ghc --make -fforce-recomp fib.hs -o fibh
[1 of 1] Compiling Main             ( fib.hs, fib.o )
Linking fibh ...
$ time ./fibh 40
165580141

real    0m0.013s
user    0m0.007s
sys 0m0.003s

$ gcc fib.c -o fibc
$ time ./fibc 40
165580141

real    0m1.495s
user    0m1.483s
sys 0m0.003s

最適化をオンにghcすると、もう少し速度が上がります。

$ ghc --make -fforce-recomp fib.hs -O3 -o fibhO
$ time ./fibhO 40
165580141

real    0m0.007s
user    0m0.002s
sys 0m0.004s

しかし、gccついに手がかりを得る。

$ gcc fib.c -O3 -o fibcO
$ time ./fibcO 40
165580141

real    0m0.007s
user    0m0.004s
sys 0m0.002s

説明は、一般的な部分式の削除に対する の警戒心だと思いますghc。「(ほとんど)すべてが式である」場合は危険であり、プログラマーがラムダの使用方法を知っていると考えられます。

于 2011-07-22T20:26:19.590 に答える
3

GHC はこれをうまくコンパイルします。次のステップは、GHC のバックエンドからの出力をマイクロ最適化することです。さまざまな LLVM フラグをいじると、ここで役立ちます。

これを行うには、ghc-core を使用してアセンブリを検査し、LLVM に対して他のフラグを試して、何が得られるかを確認します。

別のアプローチは、少量の並列処理を追加することです。

于 2011-07-16T08:56:54.047 に答える
1

これを試して:

fibs :: [Integer]
fibs = 0:1:zipWith (+) fibs (tail fibs)

fib n = fibs !! n

$ time ./fib 10000
  33644[...]6875
  ./fib 10000  0.03s user 0.01s system 89% cpu 0.039 total

(それは古き良きAthlon64 3200+にあります)

あなたが使用しているバージョンは、すべてnの 、計算fib (n-1)およびfib (n-2)です。つまり、ほぼ三角形の性質の複雑さがあります。上記のバージョンは線形です。各 fib は一度だけ計算されます。非 Haskell プログラミングのハイブマインドがどう考えているように見えても、Haskell は自動的にメモ化を行いません (とにかく、一般的には、以前の優れた動的プログラミングよりも遅くなります)。

Haskell Wikiには、さらに高速な (数学のトリックを使用した) バージョンの fibonnaci があります。

C バージョンを非再帰に変更すると、Haskell と C の両方のパフォーマンスが非常に似ていることがわかると思います。タイトなループは最適化が簡単です。

于 2011-07-16T11:04:13.327 に答える