web-dev-qa-db-ja.com

関数型言語は本質的に遅いですか?

関数型言語がベンチマークで常にCに遅れをとっているのはなぜですか?静的に型付けされた関数型言語を使用している場合は、Cと同じコードにコンパイルすることも、コンパイラーがより多くのセマンティクスを使用できるため、さらに最適化されたコードにコンパイルすることもできます。すべての関数型言語がCよりも遅いように見えるのはなぜですか。また、ガベージコレクションとヒープの過度の使用が常に必要なのはなぜですか。

メモリ割り当てが最小限に抑えられ、生成されたマシンコードが無駄のない高速な組み込み/リアルタイムアプリケーションに適した関数型言語を知っている人はいますか?

43
Steve

関数型言語は本質的に遅いですか?

ある意味ではそうです。それらには、アセンブラを手動で使用して理論的に達成できるものよりも必然的にオーバーヘッドが追加されるインフラストラクチャが必要です。特に、ファーストクラスの字句クロージャは、値をスコープ外で実行できるため、ガベージコレクションでのみ適切に機能します。

関数型言語がベンチマークで常にCに遅れをとっているのはなぜですか?

まず、選択バイアスに注意してください。 Cは、ベンチマークスイートの最小公分母として機能し、達成できることを制限します。 Cと関数型言語を比較するベンチマークがある場合、それはほぼ間違いなく非常に単純なプログラムです。おそらく非常に単純なので、今日ではほとんど実用的ではありません。単なるベンチマークとしてCを使用して、より複雑な問題を解決することは実際には実行可能ではありません。

この最も明白な例は並列処理です。今日、私たちは皆マルチコアを持っています。私の電話でさえマルチコアです。マルチコアの並列処理はCで難しいことで有名ですが、関数型言語では簡単です(F#が好きです)。他の例には、永続的なデータ構造から恩恵を受けるものが含まれます。元に戻すバッファは、純粋に関数型のデータ構造では簡単ですが、Cのような命令型言語では膨大な量の作業になる可能性があります。

すべての関数型言語がCよりも遅いように見えるのはなぜですか。また、ガベージコレクションとヒープの過度の使用が常に必要なのはなぜですか。

関数型言語は、Cでうまく書くのに十分簡単なコードを比較するベンチマークしか表示されず、関数型言語がExcelで始まるより重要なタスクを比較するベンチマークは表示されないため、遅く見えるでしょう。

ただし、今日の関数型言語でおそらく最大のボトルネックは何であるか、つまり割り当て率が高すぎることを正しく特定しました。よくやった!

関数型言語が非常に多く割り当てられる理由は、歴史的な理由と固有の理由に分けることができます。

歴史的に、LISPの実装は50年前からlotのボクシングを行ってきました。この特徴は、LISPのような中間表現を使用する他の多くの言語に広がりました。何年にもわたって、言語の実装者は、言語の実装における複雑さの迅速な解決策として、ボクシングに継続的に頼ってきました。オブジェクト指向言語では、デフォルトでは、明らかにスタック割り当てが可能な場合でも、すべてのオブジェクトを常にヒープ割り当てします。その後、効率の負担がガベージコレクターに押し付けられ、通常はバンプ割り当てのナーサリー世代を使用することで、スタック割り当てに近いパフォーマンスを達成できるガベージコレクターの構築に多大な労力が費やされました。さまざまな要件に最適化されたボクシングやガベージコレクターの設計を最小限に抑える関数型言語の設計の研究には、さらに多くの努力を払う必要があると思います。

世代別ガベージコレクターは、スタック割り当てとほぼ同じ速度で実行できるため、ヒープ割り当てが多い言語に最適です。しかし、それらは他の場所でかなりのオーバーヘッドを追加します。今日のプログラムでは、キューなどのデータ構造(並行プログラミングなど)を使用することが増えており、これらは世代別のガベージコレクターに病理学的な動作をもたらします。キュー内のアイテムが第1世代よりも長生きする場合、それらはすべてマークされ、すべてコピーされ(「退避」)、古い場所へのすべての参照が更新され、収集の対象になります。これは、必要な速度の約3倍です(たとえば、Cと比較して)。 Beltway (2002)や Immix (2008)のようなマーク領域コレクターは、保育園がまるで収集できる領域に置き換えられるため、この問題を解決できる可能性があります。保育園であったか、ほとんど到達可能な値が含まれている場合は、別の地域に置き換えて、ほとんど到達不可能な値が含まれるまで熟成させることができます。

C++がすでに存在しているにもかかわらず、Javaの作成者は、ジェネリックスに型消去を採用するという間違いを犯し、不要なボクシングを引き起こしました。たとえば、 。NETでJVMよりも17倍高速に実行される単純なハッシュテーブルをベンチマークしました 一部は.NETがこの間違いを犯さなかったため(洗練されたジェネリックを使用)、また.NETには値型があるためです。私は実際にJavaを遅くしたことでLISPを非難している。

最新の関数型言語の実装はすべて、過度にボックス化され続けています。 ClojureやScalaなどのJVMベースの言語は、ターゲットとするVMが値型を表現することさえできないため、選択肢がほとんどありません。 OCamlはコンパイルプロセスの早い段階で型情報を流し、ポリモーフィズムを処理するために実行時にタグ付き整数とボックス化に頼ります。その結果、OCamlはしばしば個々の浮動小数点数をボックス化し、常にタプルをボックス化します。たとえば、OCamlのトリプルバイトは、64ビットヘッダーと192ビット本体を含むヒープ割り当てブロックへのポインター(実行時に繰り返しチェックされる暗黙の1ビットタグが埋め込まれている)で表されます。 3つのタグ付き63ビット整数(3つのタグは実行時に繰り返し検査されます!)。これは明らかに非常識です。

関数型言語でのボックス化解除の最適化についていくつかの作業が行われていますが、実際に注目を集めることはありませんでした。たとえば、Standard ML用のMLtonコンパイラは、高度なボックス化解除の最適化を行うプログラム全体の最適化コンパイラでした。悲しいことに、それはその時代の前であり、「長い」コンパイル時間(おそらく現代のマシンでは1秒未満です!)は人々がそれを使用することを思いとどまらせました。

この傾向を打破した唯一の主要なプラットフォームは.NETですが、驚くべきことに、それは偶然だったようです。値型のキーと値に対して非常に高度に最適化されたDictionary実装があるにもかかわらず(ボックス化されていないため)、Eric LippertのようなMicrosoftの従業員は引き続き 値型に関する重要なことはパスであると主張します) -値によるセマンティクス ボックス化されていない内部表現に由来するパフォーマンス特性ではありません。エリックは間違っていることが証明されているようです。多くの.NET開発者は、値渡しよりも開封に関心があるようです。実際、ほとんどの構造体は不変であるため、参照透過性であるため、値渡しと参照渡しの間に意味上の違いはありません。パフォーマンスが表示され、構造体によってパフォーマンスが大幅に向上します。保存されたスタックオーバーフローや構造体のパフォーマンスは、 Rapid Addition's !のような商用ソフトウェアでのGCレイテンシーを回避するために使用されます。

関数型言語による大量の割り当てのもう1つの理由は固有のものです。ハッシュテーブルのような命令型データ構造は、内部で巨大なモノリシック配列を使用します。これらが永続的である場合、更新が行われるたびに巨大な内部配列をコピーする必要があります。したがって、バランスの取れたバイナリツリーのような純粋に機能的なデータ構造は、コレクションのあるバージョンから次のバージョンへの再利用を容易にするために、ヒープに割り当てられた多くの小さなブロックに断片化されます。

Clojureは、辞書などのコレクションが初期化中にのみ書き込まれ、その後ロットから読み取られる場合に、この問題を軽減するために巧妙なトリックを使用します。この場合、初期化ではミューテーションを使用して「舞台裏」の構造を構築できます。ただし、これは増分更新には役立ちません。また、結果のコレクションは、必須のコレクションよりも読み取りが大幅に遅くなります。利点として、純粋に機能的なデータ構造は永続性を提供しますが、命令型のデータ構造は提供しません。ただし、実際には永続性の恩恵を受ける実用的なアプリケーションはほとんどないため、これは多くの場合有利ではありません。したがって、命令型に簡単に移行してメリットを享受できる、不純な関数型言語への欲求。

メモリ割り当てが最小限に抑えられ、生成されたマシンコードが無駄のない高速な組み込み/リアルタイムアプリケーションに適した関数型言語を知っている人はいますか?

まだ読んでいない場合は、ErlangとOCamlを見てください。どちらもメモリに制約のあるシステムには適していますが、どちらも特に優れたマシンコードを生成しません。

63
Jon Harrop

本質的に何もありません。これは、 interpretedOCamlが同等のCコードよりも高速に実行される の例です。これは、OCamlオプティマイザーが利用できるさまざまな情報を持っているためです。言語の違いによる。もちろん、OCamlがCよりも断固として速いという一般的な主張をするのはばかげているでしょう。要点は、それはあなたが何をしているか、そしてあなたがそれをどのように行うかに依存するということです。

とは言うものの、 OCaml は、純粋さとは対照的に、 パフォーマンス 用に実際に設計された(ほとんど)関数型言語の例です。

18
Craig Stuntz

関数型言語では、言語抽象化のレベルで表示される可変状態を排除する必要があります。したがって、命令型言語によってその場で変更されるデータは、代わりにコピーする必要があり、そのコピーで変更が行われます。簡単な例については、Haskell vs.Cのクイックソートを参照してください。

さらに、free()は副作用があるため純粋関数ではないため、ガベージコレクションが必要です。したがって、言語抽象化のレベルで副作用を伴わないメモリを解放する唯一の方法は、ガベージコレクションを使用することです。

もちろん、原則として、十分にスマートなコンパイラは、このコピーの多くを最適化することができます。これはすでにある程度行われていますが、コンパイラを十分にスマートにして、そのレベルでコードのセマンティクスを理解するのは非常に困難です。

11
dsimcha

Cは基本的にアセンブラ用のマクロのセットであるため、高速です:) Cでプログラムを作成する場合、「舞台裏」はありません。それを実行するときが来たときにメモリを割り当て、同じ方法で解放します。これは、予測可能性が重要な(実際には何よりも)リアルタイムアプリケーションを作成する場合の大きな利点です。

また、Cコンパイラは、言語自体が単純であるため、一般的に非常に高速です。タイプチェックも行いません:)これは、エラーを見つけにくくするのも簡単であることを意味します。型チェックがないことの利点は、たとえば関数名をその名前と一緒にエクスポートできることです。これにより、Cコードを他の言語のコードと簡単にリンクできます。

9
Emiliano

簡単な答え:Cは速いからです。のように、途方もなくばかばかしいほど速く。言語は、Cによって後部に渡されるために、単に「遅い」必要はありません。

Cが高速である理由は、Cが非常に優れたコーダーによって作成され、gccが数十年にわたって最適化され、99%の言語よりも優れたコーダーによって最適化されているためです。

つまり、非常に特殊な関数型プログラミング構造を必要とする特殊なタスクを除いて、Cに勝るものはありません。

8
Jens Roland

手続き型言語の制御フローは、現代のコンピューターの実際処理パターンとはるかによく一致します。

Cは、コンパイルによって生成されるアセンブリコードに非常に密接にマッピングされるため、「クロスプラットフォームアセンブリ」というニックネームが付けられています。コンピューターメーカーは、アセンブリコードを可能な限り高速に実行するために数十年を費やしてきたため、Cはこの生の速度をすべて継承しています。

それに比べて、関数型言語の副作用がなく、固有の並列処理は、単一のプロセッサにまったくマッピングされません。関数を呼び出すことができる任意の順序は、CPUのボトルネックまでシリアル化する必要があります。非常に巧妙なコンパイルがなければ、コンテキストの切り替えが発生しますall時間、プリフェッチは行われませんあなたは絶えずあちこちを飛び回っているので仕事をします...基本的に、コンピュータメーカーがニースの予測可能な手続き型言語のために行ったすべての最適化作業はほとんど役に立たないです。

しかしながら! (1つまたは2つのターボチャージャー付きコアではなく)多くのそれほど強力ではないコアへの移行に伴い、関数型言語shouldは、自然に水平方向にスケーリングするため、ギャップを埋め始めます。

7
James Brady

今のところ、関数型言語は業界のプロジェクトではあまり使用されていないため、オプティマイザーに十分な真剣な作業が行われていません。また、命令型ターゲットの命令型コードを最適化する方がおそらくはるかに簡単です。

関数型言語には、すぐに命令型言語をしのぐことができる1つの偉業があります。それは、自明な並列化です。

簡単という意味ではささいなことではありませんが、開発者が考える必要なしに、言語環境に組み込むことができます。

Cのようなスレッドに依存しない言語での堅牢なマルチスレッドのコストは、多くのプロジェクトにとって法外なものです。

5
peterchen

さて、HaskellはGCCのC++よりも1.8倍遅いだけです。これは、一般的なベンチマークタスクのGCCのC実装よりも高速です。これにより、Haskellは非常に高速になり、C#(つまりMono)よりもさらに高速になります。

相対的な言語速度

  • 1.0 C++ GNU g ++
  • 1.1 C GNU gcc
  • 1.2 ATS
  • 1.5 Java 6 -server
  • 1.5クリーン
  • 1.6 Pascal Free Pascal
  • 1.6 Fortran Intel
  • 1.8 Haskell GHC
  • 2.0 C#モノ
  • 2.1 Scala
  • 2.2 Ada 2005 GNAT
  • 2.4 LISP SBCL
  • 3.9 Lua LuaJIT

ソース

記録として、私はiPhoneでLua for Gamesを使用しているので、HaskellまたはLISPの方が高速なので、必要に応じて簡単に使用できます。

4
Robert Gould

私はtuinstoelに同意しません。重要な問題は、関数型言語を使用すると、どの関数型言語を使用する場合でも、開発時間が短縮され、コードが高速化されるかどうかです。私が何を意味するのかを垣間見るには、 ウィキペディアの効率の問題のセクション を参照してください。

3
Szymon Rozga

実行可能ファイルのサイズが大きくなるもう1つの理由は、遅延評価と非厳密性である可能性があります。コンパイラは、特定の式が評価されるコンパイル時に理解できないため、これを処理するために実行可能ファイルにランタイムが詰め込まれます(いわゆるthunksの評価を呼び出すため)。パフォーマンスに関しては、怠惰は良いことも悪いこともあります。一方では、追加の潜在的な最適化が可能になりますが、他方では、コードサイズが大きくなる可能性があり、プログラマーは悪い決定を下す可能性が高くなります。 Haskellのfoldl vs. foldr vs. foldl ' vs. foldr'を参照してください。