web-dev-qa-db-ja.com

なぜHaskell(GHC)はこんなに速いの?

Haskell(GHCコンパイラを使用)は 予想よりはるかに速い です。正しく使用すると、低レベル言語に近いものになる可能性があります。 (Haskellersがするのが一番好きなことは、Cの5%以内に到達することです(あるいはそれに勝ることもできますが、GHCはHaskellをCにコンパイルするので、効率の悪いCプログラムを使用していることを意味します))。

Haskellは宣言型で、ラムダ計算に基づいています。機械アーキテクチャは、大体チューリング機械に基づいているため、明らかに不可欠です。確かに、Haskellには特定の評価順序さえありません。また、マシンデータ型を扱う代わりに、代数データ型を常に作ります。

もっとも奇妙なのは高階関数です。その場で関数を作成し、それらを投げ捨てると、プログラムが遅くなると思うでしょう。 しかし、高階関数を使うと実際にはHaskellが速くなります。 実際、Haskellのコードを最適化するには、もっとマシン風ではなくもっとエレガントで抽象的にする必要があるようです。 Haskellのより高度な機能はどれもその性能に影響を与えないように思われます

残念だが残念だが、ここに私の質問:Haskell(GHCでコンパイルされた)は、その抽象的な性質と物理マシンとの違いを考えると、なぜこんなに速いのですか?

注:Cや他の命令型言語がTuring Machinesに多少似ていると言う理由は(ただしHaskellがLambda Calculusに似ている限りではありません)、命令型言語では有限の状態数(別名行番号)があるからです。状態(現在のテープ)がテープに何をすべきかを決定するように、テープ(ラム)と共に。 Turing Machinesからcomputerへの移行については、ウィキペディアのエントリ Turing machine equivalent を参照してください。

200
PyRulez

これは少し意見に基づいていると思います。しかし、私は答えようとします。

ディートリッヒ・エップに同意します。GHCを高速にするのは、いくつかのものの組み合わせです。

何よりもまず、Haskellは非常に高レベルです。これにより、コンパイラはコードを壊すことなく積極的な最適化を実行できます。

SQLについて考えてください。さて、SELECTステートメントを記述すると、 /のような命令ループ、のように見えますが、/ではありません。 のように見えるかもしれませんそのテーブルのすべての行をループして、指定された条件に一致する行を見つけようとしますが、実際には "(DBエンジン)は、代わりにインデックスルックアップを実行できます。これは、完全に異なるパフォーマンス特性を持っています。しかし、SQLは非常に高レベルであるため、「コンパイラ」はまったく異なるアルゴリズムを代用し、複数のプロセッサまたはI/Oチャネルまたは servers 全体を透過的に適用できます。

Haskellは同じものだと思います。 think は、入力リストを2番目のリストにマッピングし、2番目のリストを3番目のリストにフィルターし、結果のアイテム数をカウントするようHaskellに要求しただけです。しかし、GHCが背後でストリームフュージョンリライトルールを適用することはありませんでした。退屈で、エラーが発生しやすく、手で書くのが維持できない。これは、コードに低レベルの詳細がないためにのみ可能です。

それを見るもう一つの方法は…なぜすべきではない Haskellは速いのでしょうか?それが遅くなるのは何ですか?

PerlやJavaScriptのようなインタプリタ言語ではありません。 JavaやC#のような仮想マシンシステムでもありません。ネイティブマシンコードまでコンパイルされるため、オーバーヘッドは発生しません。

OO言語[Java、C#、JavaScript…]とは異なり、Haskellには完全な型消去(C、C++、Pascalなど)があります。すべての型チェックはコンパイル時にのみ行われます。したがって、実行速度を遅くする実行時の型チェックもありません。 (その点については、null-pointerチェックはありません。たとえば、Javaでは、JVMはnullポインターをチェックし、例外がある場合は例外をスローする必要があります。Haskellはそのチェックに煩わされる必要はありません。)

「実行時にオンザフライで関数を作成する」のは遅いように思えますが、よく見ると実際にはそうしていません。 のように見えるかもしれませんあなたはそうですが、あなたはそうではありません。 (+5)と言うと、ソースコードにハードコードされています。実行時に変更することはできません。したがって、実際には動的関数ではありません。カリー化された関数でさえ、パラメーターをデータブロックに保存するだけです。すべての実行可能コードは、実際にはコンパイル時に存在します。実行時の解釈はありません。 (「評価関数」を持つ他の言語とは異なります。)

パスカルについて考えてください。古いもので、実際に使用している人はいませんが、Pascalが slow であると文句を言う人はいません。それについて嫌いなものはたくさんありますが、遅さは実際にはそれらの1つではありません。 Haskellは、手動のメモリ管理ではなくガベージコレクションを行うことを除いて、Pascalとは異なることを実際には行っていません。また、不変データにより、GCエンジンの最適化がいくつか可能になります(遅延評価はやや複雑になります)。

Haskellは見た目高度で洗練された高レベルであり、誰もが「ああ、すごい、これは本当に強力で、それは驚くほど遅いに違いない」と思う! "しかしそうではありません。または、少なくとも、あなたが期待するような方法ではありません。はい、それは素晴らしい型システムを持っています。しかし、あなたは何を知っていますか?それはすべてコンパイル時に起こります。実行時には、それはなくなっています。はい、複雑なADTをコード行で構築できます。しかし、あなたは何を知っていますか? ADTは、普通のC union of structsです。これ以上何もない。

本当のキラーは怠evaluationな評価です。コードの厳格さ/怠rightさを正しく理解すれば、まだエレガントで美しい愚かな高速コードを書くことができます。しかし、もしこれが間違っていると、プログラムは数千倍遅くなります、そしてなぜこれが起こっているのかは本当に明白ではありません。

たとえば、ファイルに各バイトが何回現れるかを数えるために、ささいな小さなプログラムを書きました。 25KBの入力ファイルの場合、プログラムは20分を実行して飲み込みました6ギガバイトのRAM!それはばかげている!!しかし、その後、私は問題が何であるかを認識し、単一の強打パターンを追加し、実行時間を.02秒に落としました。

This は、Haskellが予想外にゆっくりと進む場所です。そしてそれに慣れるには確かに時間がかかります。しかし、時間が経つにつれて、非常に高速なコードを書くのが簡単になります。

Haskellの高速化の理由は何ですか?純度。静的タイプ。怠惰。しかし、何よりも、コンパイラーがコードの期待を壊すことなく実装を根本的に変更できるほど十分に高いレベルである。

しかし、それは私の意見だと思う…

223

長い間、関数型言語は速くなることはできないと考えられていました - そして特に怠惰な関数型言語。しかしこれは、それらの初期の実装が本質的に解釈され、真にコンパイルされていなかったためです。

グラフ縮小を基にしたデザインの第2の波が生まれ、はるかに効率的なコンパイルの可能性が切り開かれました。 Simon Peyton Jonesは、この研究について2冊の本で書いています。 関数型プログラミング言語の実装 および 関数型言語の実装:チュートリアル (前者はWadlerとHancockによるセクション、後者は書かれています) David Lesterと) (Lennart Augustssonはまた、前の本の大きな動機の1つは、彼のLMLコンパイラ(これは広くコメントされていない)がそのコンパイルを達成した方法を説明していることだと私に知らせた)。

これらの作品で説明されているようなグラフ縮小アプローチの背後にある重要な概念は、プログラムを命令のシーケンスとしてではなく、一連のローカルでevaluateとなる依存関係グラフについて考えることです。削減2つ目の重要な洞察は、そのようなグラフの評価と解釈する必要はないということですが、その代わりにグラフ自体をビルトインコードにすることができます。特に、グラフのノードを「値または「オペコード」と操作する値」のいずれかとしてではなく、呼び出されると目的の値を返す関数として表すことができます。最初に呼び出されたときに、サブノードに値を要求してからそれらを処理します。そしてそれは単に「結果を返す」という新しい命令で上書きされます。

これは、GHCが今日どのように機能するのかについての基本を説明する後の論文で説明されています(多くの様々な調整を法として)。 "ストックハードウェアへの遅延関数言語の実装:Spineless Tagless G-Machine"。 。 GHCの現在の実行モデルは GHC Wiki でより詳細に文書化されています。

したがって、洞察力は、私たちが機械の動作の仕方にとって「基本」と考える「データ」と「コード」の厳密な区別はそれらが動作しなければならない方法ではなく、私たちのコンパイラによって課されるということです。だから私たちはそれを捨て去ることができ、そして自己修正コード(実行ファイル)を生成するコード(コンパイラ)を持つことができ、そしてそれはすべてとてもうまく働くことができます。

したがって、マシンアーキテクチャはある意味で必須ではありますが、言語は従来のCスタイルのフロー制御のようには見えない非常に驚くべき方法でそれらにマッピングされる可能性があります。効率的です。

これに加えて、それがより広い範囲の「安全な」変換を可能にするので、特に純度によって開かれた他の多くの最適化があります。状況を改善し悪化させないためにこれらの変換を適用するタイミングと方法はもちろん経験的な問題であり、これと他の多くの小さな選択に関して、理論的な作業と実用的なベンチマークの両方に長年の作業が費やされました。したがって、これももちろん役割を果たします。この種の研究の良い例を提供する論文は、「 速いカレーを作る:プッシュ/エンター対評価/高次言語への適用」です。

最後に、このモデルでは間接的な方法によるオーバーヘッドが依然として発生することに注意してください。これは、物事を厳密に行うことが「安全」であり、したがってグラフの間接性を排除することが「安全」であることがわかっている場合には回避できます。厳密さ/要求を推論するメカニズムは、 GHC Wiki でやや詳細に文書化されています。

61
sclv

まあ、ここにコメントすることがたくさんあります。私はできる限り答えるようにします。

正しく使用すると、低レベル言語に近いものになる可能性があります。

私の経験では、多くの場合、通常Rustの2倍のパフォーマンスを達成することが可能です。しかし、低レベルの言語と比較してパフォーマンスが低い(広範な)ユースケースもいくつかあります。

gHCはHaskellをCにコンパイルしているので、それはあなたが非効率的なCプログラムを使っていることを意味します。

それは完全に正しいというわけではありません。 HaskellはC Cのサブセット)にコンパイルし、ネイティブコード生成プログラムを介してAssemblyにコンパイルされます。ネイティブコードジェネレータは通常、Cコンパイラよりも高速なコードを生成します。通常のCコンパイラでは不可能な最適化を適用できるためです。

機械アーキテクチャは、大体チューリング機械に基づいているため、明らかに不可欠です。

特に最近のプロセッサは命令を順不同にそしておそらく同時に評価するので、それはそれについて考えるための良い方法ではありません。

確かに、Haskellには特定の評価順序さえありません。

実際、Haskellは評価順序を暗黙的に定義します

また、マシンデータ型を扱う代わりに、代数データ型を常に作ります。

あなたが十分に高度なコンパイラを持っていれば、それらは多くの場合に対応します。

その場で関数を作成し、それらを投げ捨てると、プログラムが遅くなると思うでしょう。

Haskellはコンパイルされているので、高階関数は実際にはその場で作成されません。

それはHaskellのコードを最適化するように思われます、あなたはより多くのマシンのような代わりに、よりエレガントで抽象的なものにする必要があります。

一般に、Haskellでコードをより「マシンライク」にすることは、パフォーマンスを向上させるための非生産的な方法です。しかし、それをより抽象的にすることも常に良い考えではありません。 であることの良い考えは、非常に最適化されている一般的なデータ構造と関数(リンクリストなど)を使用することです。

f x = [x]f = pureは、例えばHaskellではまったく同じものです。前者の場合、優れたコンパイラではパフォーマンスが向上しません。

その抽象的な性質と物理マシンとの違いを考慮して、なぜHaskell(GHCでコンパイルされた)がこんなに速いのですか?

簡単な答えは「それが正確にそれを行うように設計されていたため」です。 GHCは無脊椎無タグg-machine(STG)を使用します。あなたはそれについての論文を読むことができます---(ここ (それはかなり複雑です)。厳密性分析や 楽観的評価 など、GHCは他にも多くのことを行います。

Cや他の命令型言語がTuring Machinesにやや似ていると言う理由は(HaskellがLambda Calculusに似ているという意味ではありません)、命令型言語では有限個の状態(別名行番号)があるからです。テープ(RAM)を使用して、状態と現在のテープによってテープに対する処理を決定します。

それでは混乱のポイントは、可変性が遅いコードにつながるべきであるということですか? Haskellの怠惰は実際にはあなたが考えているほど可変性が問題にならないことを意味します。加えてそれは高水準なので、コンパイラが適用できる多くの最適化があります。したがって、レコードをインプレースで変更しても、Cなどの言語よりも遅くなることはめったにありません。

11
user8174234