なぜHaskell(GHC)はとても速いのですか?


246

Haskell(GHCコンパイラを使用)は、予想よりもはるかに高速です。正しく使用すると、低レベルの言語に近づくことができます。(Haskellersが行うのが好きなことは、Cの5%以内を試すことです(またはそれを打つこともできますが、GHCはHaskellをCにコンパイルするため、非効率的なCプログラムを使用していることになります)。私の質問は、なぜですか?

Haskellは宣言型であり、ラムダ計算に基づいています。マシンアーキテクチャは明らかにチューリングマシンに基づいているため、必須です。実際、Haskellには特定の評価順序さえありません。また、マシンデータタイプを処理する代わりに、代数データタイプを常に作成します。

最も奇妙なのは高次関数です。関数をその場で作成し、それらを使い果たすと、プログラムが遅くなると考えるでしょう。しかし、より高次の関数を使用すると、実際にHaskellが高速になります。実際、Haskellコードを最適化するには、マシンのようにではなく、よりエレガントで抽象的なものにする必要があるようです。Haskellのより高度な機能は、それを改善しなければ、パフォーマンスに影響を与えるようには見えません。

これが不満に聞こえる場合は申し訳ありませんが、ここに私の質問があります。その抽象的な性質と物理マシンとの違いを考慮して、なぜHaskell(GHCでコンパイル)がそれほど高速なのですか?

注:私がCや他の命令型言語がチューリングマシンにいくらか似ている(しかし、HaskellがLambda計算に似ているというわけではない)理由は、命令型言語では有限数の状態(別名行番号)があるためです、およびテープ(RAM)とともに、状態と現在のテープがテープに対して何を行うかを決定します。チューリングマシンからコンピューターへの移行については、Wikipediaのエントリ「チューリングマシンの同等物」を参照してください。


27
「GHCはHaskellをCにコンパイルするため」-コンパイルしません。GHCには複数のバックエンドがあります。最も古いもの(ただしデフォルトではない)はCジェネレーターです。それはIRのCmmコードを生成しますが、それは通常期待される「Cへのコンパイル」ではありません。(downloads.haskell.org/~ghc/latest/docs/html/users_guide/...
viraptor

19
Simon Payton Jones(GHCの主要な実装者)による関数型プログラミング言語の実装を読むことを強くお勧めします。
Joe Hillenbrand、2016年

94
どうして?25年間のハードワーク。
2016年

31
「それに対する事実の答えがあるかもしれないとしても、それは意見を求めるだけであるでしょう。」-これが、質問を閉じる最悪の理由です。それがあるためかもしれ良い答えを持っているが、それは潜在的にも低品質のものを魅了します。おい!私はたまたま、学術研究について、そして特定の進展が起こったときに、歴史的で事実に基づいた良い答えを並べました。しかし、この質問は質の低い答えを引き付けるかもしれないと心配しているので、私はそれを投稿することはできません。繰り返しますが、
sclv 2016年

7
@cimmanon 関数型コンパイラがどのように機能するかについての詳細の基本を説明するには、1か月またはいくつかのブログ投稿が必要です。グラフマシンをストックハードウェアにきれいに実装する方法を
大まかに概説

回答:


264

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

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

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

Haskellも同じだと思います。入力リストを2番目のリストにマップし、2番目のリストを3番目のリストにフィルター処理して、結果のアイテム数をカウントするようにHaskellに要求したと思うかもしれません。しかし、GHCが背後でストリームフュージョンの書き換えルールを適用し、全体を単一のタイトなマシンコードループに変換して、割り当てなしでデータを1回パスするだけですべての処理を実行することはありませんでした。手作業で作成するのは面倒で、エラーが発生しやすく、保守が困難です。これは、コードに低レベルの詳細がないため、本当に可能です。

それを見る別の方法は...なぜHaskellは高速であるべきではないのでしょうか?どうすれば遅くなりますか?

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

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

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

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

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

本当のキラーは怠惰な評価です。コードの厳密性/遅延性を正しく理解すると、エレガントで美しい、愚かな高速コードを作成できます。しかし、これを間違えると、プログラムは何千倍も遅くなります。そして、これが起こっている理由は明らかではありません。

たとえば、ファイルに各バイトが出現する回数を数える簡単な小さなプログラムを作成しました。25KBの入力ファイルの場合、プログラムの実行に20分かかり、6ギガバイトのRAM を飲み込みました。それはばかげている!しかし、それから私は問題が何であるかを理解し、単一の強打パターンを追加し、実行時間が0.02秒に低下しました。

これは、Haskellが予期せずゆっくりと進むところです。そして、それに慣れるまでには確かにしばらく時間がかかります。しかし、時間が経つにつれて、本当に高速なコードを書くことがより簡単になります。

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

でもそれは私の意見だと思います...


13
@cimmanon私はそれが純粋に意見に基づいているとは思いません。これは、他の人がおそらく答えを求めている興味深い質問です。しかし、他の有権者の考えを見ることができると思います。
MathematicalOrchid

8
@cimmanon-その検索では1つ半のスレッドしか提供されず、すべてのスレッドがレビュー監査に関係しています。スレッドへの賛成投票には、「理解できないもののモデレートをやめてください」と書かれています。これに対する答えが必然的に広すぎると誰かが思うなら、その答えは広すぎないので、驚かれ、その答えを楽しむでしょう。
sclv 2016年

34
「たとえば、Javaでは、JVMはnullポインターをチェックし、それを無視する場合は例外をスローする必要があります。」Javaの暗黙のnullチェックは(ほとんど)コストがかかりません。Javaの実装では、仮想メモリを利用してnullアドレスを不足しているページにマッピングできるため、nullポインターを逆参照すると、CPUレベルでページ違反がトリガーされ、Javaが高レベルの例外としてキャッチしてスローします。したがって、ほとんどのnullチェックは、CPUのメモリマッピングユニットによって無料で行われます。
Boann、2016年

4
@cimmanon:多分それは、Haskellユーザーが実際には友好的なオープンマインドな人々の集まりである唯一のコミュニティであるように思われるためです。彼らが得るあらゆる機会でお互いに新しいものを引き裂きます...これはあなたが「正常」であると考えるもののようです。
Evi1M4chine

14
@MathematicalOrchid:実行に20分かかった元のプログラムのコピーはありますか?なぜこんなに遅いのかを研究することは非常に有益だと思います。
ジョージ

79

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

グラフの縮小に基づいて設計の第2の波が浮上し、より効率的なコンパイルの可能性が開かれました。サイモンペイトンジョーンズは彼の2冊で、この研究について書いた関数型プログラミング言語の実装チュートリアル:関数型言語の実装(Wadlerとハンコックによってセクション前者、とDavidレスターで書か後者)。(Lennart Augustssonは、前の本の主な動機の1つは、広範囲にコメントされていない彼のLMLコンパイラーがそのコンパイルを達成した方法を説明していることも知らせました)。

これらの作品で説明されているようなグラフ簡約アプローチの背後にある重要な概念は、プログラムを一連の命令と見なすのではなく、一連の局所簡約によって評価される依存グラフのことです。2つ目の重要な洞察は、そのようなグラフの評価を解釈する必要はなく、グラフ自体をコード構築できることです。特に、グラフのノードを「値または「オペコード」と操作する値のいずれかとして」ではなく、呼び出されたときに目的の値を返す関数として表すことができます。それが呼び出された最初の時間は、それはそれらの値のためのサブノードを要求し、それらの上で動作、その後、それだけで結果を返す」という新しい命令で自身を上書きします。

これについては、GHCが今日でもどのように機能するかについての基本を説明した後のペーパーで説明されています(ただし、多くのさまざまな微調整が行われています)。GHCの現在の実行モデルは、GHC Wikiで詳細に文書化されています。

したがって、洞察は、マシンがどのように動作するかを「基本」として考える「データ」と「コード」の厳密な区別は、それらが動作する必要がある方法ではなく、コンパイラーによって課されるということです。したがって、それを破棄して、自己変更コード(実行可能ファイル)を生成するコード(コンパイラー)を用意すれば、すべてうまく機能します。

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

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

最後に、このモデルでも、間接化のためにオーバーヘッドが発生することに注意してください。これは、物事を厳密に実行することが「安全」であり、したがってグラフの間接指示を排除することがわかっている場合には回避できます。厳格さ/要求を推測するメカニズムは、GHC Wikiで再び詳細に文書化されています。


2
そのデマンドアナライザーリンクは、金でその価値があります!最後に、基本的に不可解な黒魔術であるかのように機能していないトピックについての何か。これについて聞いたことがないのですか?それは誰でも怠惰の問題に取り組む方法を尋ねるどこからでもリンクされるべきです!
Evi1M4chine

@ Evi1M4chineデマンドアナライザーに関連するリンクが表示されません。おそらく何らかの理由で失われています。誰かがリンクを元に戻したり、参照を明確にしたりできますか?なかなか面白いですね。
クリスP

1
@CrisP最後のリンクは言及されているものだと思います。GHCのデマンドアナライザーに関するGHC Wikiのページに移動します。
Serp C

@サーペンタインクーガー、クリスP:うん、それは私が意味したことです。
Evi1M4chine 2017年

19

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

正しく使用すると、低レベルの言語に近づくことができます。

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

またはそれを打つこともできますが、GHCはHaskellをCにコンパイルするため、非効率的なCプログラムを使用していることになります)

それは完全に正しいわけではありません。HaskellはC--(Cのサブセット)にコンパイルされ、ネイティブコードジェネレーターを介してアセンブリにコンパイルされます。ネイティブコードジェネレーターは通常のCコンパイラーよりも高速なコードを生成します。これは、通常のCコンパイラーではできないいくつかの最適化を適用できるためです。

マシンアーキテクチャは明らかにチューリングマシンに基づいているため、必須です。

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

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

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

また、マシンデータタイプを処理する代わりに、代数データタイプを常に作成します。

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

関数をその場で作成し、それらを使い果たすと、プログラムが遅くなると考えるでしょう。

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

Haskellコードを最適化しているようで、より機械的なものではなく、よりエレガントで抽象的なものにする必要があります。

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

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

Haskell(GHCでコンパイル)は、その抽象的な性質と物理マシンとの違いを考えると、なぜそれほど高速なのですか?

簡単に言えば、「まさにそれを行うように設計されているからです」。GHCは、スパインレスタグレスgマシン(STG)を使用します。あなたはそれについての論文をここで読むことができます(それは非常に複雑です)。GHCは、厳密性分析や楽観的評価など、他にも多くのことを行います。

Cや他の命令型言語がチューリングマシンにいくらか似ている(しかし、HaskellがLambda Calculusに似ているというわけではない)理由は、命令型言語では、有限数の状態(別名行番号)があり、テープ(RAM)を使用して、状態と現在のテープがテープに対して何を行うかを決定します。

混乱のポイントは、その可変性によりコードが遅くなることでしょうか?Haskellの遅延は、実際には、可変性は思ったほど重要ではないことを意味します。さらに、高レベルであるため、コンパイラーが適用できる多くの最適化があります。そのため、レコードをインプレースで変更することは、Cなどの言語よりも遅くなることはめったにありません。


3

なぜHaskell(GHC)はとても速いのですか?

最後にHaskellのパフォーマンスを測定してから、何かが劇的に変化したに違いありません。例えば:

それで何が変わったのですか?質問も現在の回答も、検証可能なベンチマークやコードにさえ言及していないことに気づきました。

Haskellersが好きなことは、Cの5%以内を取得することです

誰もがその近くのどこかに行ったという検証可能な結果への言及はありますか?


6
誰かが鏡の前で再びハロップの名前を3回言ったか?
チャックアダムス

2
10xではありませんが、それでも、このエントリ全体宣伝宣伝と宣伝です。GHCは確かに速度の点でCに近づいたり、時々それを克服したりする能力がありますが、通常はC自体でのプログラミングとそれほど変わらない、かなり複雑な低レベルのプログラミングスタイルが必要です。残念ながら。コードのレベルが高いほど、通常は遅くなります。スペースリーク、便利であるがパフォーマンスの低いADTタイプ(代数的抽象ではなく、約束どおり)などなど
Will Ness

1
今日私はそれをchrispenner.ca/posts/wcで見たので、私はこれを投稿しています。Haskellで書かれたwcユーティリティの実装で、おそらくcバージョンに勝っています。
Garrison、

3
リンクをありがとう@Garrison 。80行は、私が「C自体のプログラミングとそれほど変わらない低レベルのプログラミングスタイル」と呼んだものです。。「より高いレベルのコード」、それは「愚かな」ことでしょうfmap (length &&& length . words &&& length . lines) readFile。場合はそれがより速く(あるいは同等の)がCだった、ここでの誇大広告は完全に正当化されるだろう、その後。CのようにHaskellでもスピードのために懸命に努力する必要があるのがポイントです。
ネスは

2
Redditreddit.com/r/programming/comments/dj4if3/…でのこのディスカッションから判断すると、Haskellコードは実際にバグがあり(たとえば、行の改行が空白で開始または終了し、àで改行)、主張されたパフォーマンス結果を再現できない。
Jon Harrop、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.