目隠しバッグで背中合わせに走る

どのプログラミング言語が最速かは常に実用的ではありませんが、非常に興味深い質問です。 ベンチマークゲームサイトはまさにそれです。 プロジェクトの本質は、多くの典型的なタスクでプログラミング言語の速度を比較することです。 結果は常に予測可能であるとは限りません。 JavaScriptがCと同じくらい速い場合はどうなりますか? これはスキャンダルです!



プライドと偏見



多くの場合、実行の品質に関係なく、所有者は常に何かを迅速に実行できることを高く評価しています。 -ジェーン・オースティン



ベンチマークゲームは 、プログラミング言語の長所と短所を証明するためによく参照されます。 ただし、ここで注意する必要があります。 専門的にパフォーマンスの測定に従事している人は、このビジネスには多くの落とし穴があることを知っています。 たとえば、Java仮想マシンはウォームアップに時間がかかります。 したがって、短すぎるテストでは、結果は代表的ではありません。 幸いなことに、統計の観点から、このサイトは非常に体系的なアプローチを使用しています。



しかし、数字はまだ信頼できません。その理由は次のとおりです。







たとえば、お気に入りのプログラミング言語がCであると想像してください。同時に、比較の1つでは、CはJavaに劣り、これは大幅に2倍低くなります。 不公平! ソリューションコードをCで開くと、非常に正確に記述されていないことがわかります。明らかに、多くの改善と最適化が可能です。 同時に、腕によって自由な夕べが開かれ、テーブルにビールが何本かある場合、パッチは避けられません。 このアプローチが主な問題です。



このサイトの目的は、最適化されていない標準のソリューションを比較することです。 理想的には、すべてのプログラムが同じ標準アルゴリズムに従って実装されることが必要です。 この場合、トリック、ハッキング、非標準ライブラリなどは使用しないでください。 悲しいかな、Isaac Gouyプロジェクトの現在の所有者は、他の問題の明白なプロフェッショナリズムと徹底性にもかかわらず、まだそのような決定を許可しています。



このため、表やグラフは提供しませんが、さまざまなソリューションのコードレベルでいくつかのタスクを詳細に分析しようとします。



タスク:n-body



スウェーデンのオスカー2世は、賢明な君主でした。 彼は多くの重要な質問を心配しました、例えば、月は地球に落ちますか? 1885年、彼は三体問題が提示された数学コンテストを発表しました-地球-月-太陽系のモデリング。



勝利の解決策はアンリ・ポアンカレによって提示されましたが、それは正確ではありませんでしたが、それでも数学、特にカオス理論の発展に大きく貢献しました。 一般的な場合、問題はN体問題と呼ばれます。



benchsgameのn体タスクは、有限増分法を使用したSun-Jupiter-Saturn-Uranus-Neptuneシステムのシミュレーションです。 技術的な観点から見ると、タスクは、ネストされたループ内の少数のdouble変数に対する一連の算術演算です。



当然、インタプリタ言語では平凡な結果が表示されます 。Erlangでは3分、続いてPHPLuaPerlRubyが続き、 Pythonシリーズは13分で完了します。



コンパイルされたプログラミング言語での正直なソリューションは、 ChapelC#GoOCamlSwiftJavaFree Pascalの順序で20〜22秒の密なグループになります。 Node.jsTypeScriptLisp 、およびDartはわずかに遅れています-すべて27秒の領域です。



リーダー



Rustが予想外に良い結果を示した:正直な決定で13秒。 確かに、Rust言語開発チームが代表しています。 おそらく、ソリューションの単純さは明らかです。



傑出した勝者はFortranです:8秒、しかしキャッチがあります。 最適なソリューションは、さまざまな開発者によるコード改善の4回の反復の結果です。 開発者が書く最終的なコードが典型的なものであるかどうかは、まだ論点です。



詐欺師



Haskellのソリューションは21秒で完了しますが、4つのプロセッサコアすべてを利用しますが、これは完全に正直ではありません。



Cでは 、データ型__m128dを使用し、SSE2命令を手動で適用することにより、プログラムを最大8秒まで最適化できました。 これを正直な決定と呼ぶのは難しいです。 標準の算術では、 Cは同じ20秒で満たされます。



結論



コンパイルされたプログラミング言語は、数学計算においてほぼ同等に高速ですが、JavaScript(V8)および関連言語もそれらに帰することができます。 したがって、アプリケーションで惑星の動きを突然シミュレートする場合は、ブラウザーで実行します。 サーバーリソースの使用率に関しては、同様に高速ではるかに経済的です。



タスク:バイナリツリー



同じ深い歴史的背景はありませんが、以前のものと同じように好奇心が強いです。 問題の本質は、各親がちょうど6から22の深さの子孫を正確に2つ持っている場合の一連の完全な二分木の連続的な構築です。



このタスクは、標準のメモリ管理メカニズムを測定することを目的としているため、条件によって、言語の基本的な手段を使用して各ノードにメモリを明示的に割り当てたり解放したりする必要があります。 したがって、条件によって、サイズ8388608から1要素を差し引いたサイズの配列を割り当てることは明示的に禁止されています。



リーダー



実行モデルはjvmガベージコレクションモデルに非常に適しているため、 Java (12秒を少し上回る)が最も正直な結果を示すことは驚くことではありません。 メモリは順番に割り当てられ、大きなチャンクで解放されるため、この場合のヒープ上のJavaメモリの割り当てのコストは、スタック上のメモリの割り当てのコストに匹敵します。 ほとんど無料です。



Zero GCをオンにするだけで、さらに高速になります。 ガベージコレクターを完全に無効にします。 十分なメモリがある場合はどうでしょうか。 ところで、アイデアは新しいものではありません。 最初のLisp実装である1958は 、まさにそのようなガベージコレクターを使用していました。 システムに空きメモリがある限りメモリが割り当てられ、ガベージコレクションアルゴリズムの実装はより良い時間まで延期されました。



これに対して、ノードごとにmallocとfreeを使用する正直なCソリューションには、最大で37秒かかります。 まあ、そのようなタスク。



詐欺師



OCaml 、10秒-レイヤーごとにメモリを割り当てます。

let workers = Array.init((max_depth-d)/ 2 + 1)(fun i-> let d = d + i * 2 in(d、invoke worker d))



Rust 、6秒-Arenaの概念とマルチスレッドを使用します。

let long_lived_arena = Arena :: new(); let long_lived_tree = bottom_up_tree(&long_lived_arena、max_depth);

...

スレッド::スポーン(移動||内部(深さ、反復))



再び錆び 、4秒-Arendと並列イテレータを使用します(レーヨン::プレリュード):

let arena = Arena :: new();

let depth = max_depth + 1;

let tree = bottom_up_tree(&arena、depth);

...

let chk:i32 =(0 ... iterations).into_par_iter()。map(| _ | {

...



そして最後にC 、2秒半-apr_poolsとプリプロセッサ最適化を使用します:

apr_pool_t * thread_Memory_Pool; apr_pool_create_unmanaged(&thread_Memory_Pool);

...

#pragma omp parallel for

for(current_Tree_Depth = minimum_Tree_Depth; ...



結論



Java / C#ガベージコレクターを使用したメモリ管理モデルは、特定のタスクでの単純な手動メモリ管理よりもはるかに効率的です。



DartNode.js、およびGoのガベージコレクション管理には、おそらく改善が必要です。結果は約40秒であり、Javaと同じくらい速く動作します。 ただし、これらの言語のガベージコレクターの速度は、メモリ消費を最小限に抑えるために意図的に犠牲になる可能性があります。



メモリ管理を手動で最適化することにより、少なくとも2倍のパフォーマンスの向上を達成できますが、それほど難しくありません。



タスク:スレッドリング



リングに接続された503スレッドを作成する必要があります。 したがって、1番目のスレッドは2番目、2番目から3番目などを指し、503番目は1番目を指します。 ストリーム間でトークンを50,000,000回順に転送し、最後に受信したトークンのプロセス番号を出力する必要があります。 一種のポテトゲーム



正直な決定



適切なソリューションは、503ストリームを作成し、それらを503チャネルのリングに接続し、それらを介して円でメッセージを送信することです。 Javaの場合、これらはGoの場合はBlockingQueue、Erlangの場合は組み込みのプロセス間メッセージになります。



Rustでは約3分、 Rubyでは 5分、 C#では 6分です。 残念ながら、他の言語では、正直な解決策は一般的にそうではありません。



詐欺師



JavaはLockSupport.park()とvolatileを使用して、ペニーで3分を達成しました。 Python 3OCamlLisp、およびCに対する同様のアプローチ(Mutexを使用)は、最大2分半かかります。 すべての場合で、4つのプロセッサが平均で30%読み込まれるのは興味深いです。 セミアクティブ待機のオーバーヘッドは約5%です。



アーランソリューション-43秒、 Smalltalk -39秒、 Chapel -27秒、 Go -13秒、Haskell-9秒は考慮されません。これらの言語での実際のプロセッサ間パフォーマンス。 Goソリューションでは、通常、runtime.GOMAXPROCS(1)と記載されていますが、これは深刻な問題ではありません。 同じ成功により、5000万回の反復サイクルを単純に繰り返すことができました。



別のハックはC ++です :29秒。 解決策は、非同期入力出力のライブラリであるasio.hppに基づいています。これは、それ自体は興味がありますが、スレッド間でメッセージを送信するタスクとは関係ありません。 どうやら、F#ソリューション-18秒-は、非同期プリミティブを使用してストリームの代わりに遅延関数を定義するため、同じ原理で動作します。



リーダーではなく結論



悲しいかな、リーダーはいません。GoやErlangなどの言語では、正直な解決策が良い結果を示す必要があるため、そのような解決策は提示されていません。



マルチスレッド通信は、特に同じ物理コアで実行される場合、プログラムスレッド(Erlang、Goルーチン)ではるかに効率的です。 オペレーティングシステムレベルで実際のスレッドをジャグリングしながら、完全なコンテキストを維持および復元し、すべてのプロセスのレベルで一般的なシェダー内で優先順位を付けると、はるかに遅くなります。



実際のスレッドの代わりに非同期I / Oを使用するのは素晴らしいことですが、nginxとnode.jsの時代からすでにそれを知っていました。



一般的な結果



私は速い、私はとても速い...寝る前の寝室で、スイッチを押して、明かりが消えるまで寝る時間がある...私はとても速い。 -モハメッドアリ



残念ながら、現実を装飾したいという願望は、少なくともサイトのベンチマークゲームでは、開発者の魂の常識を勝ち取ります。 その結果、プログラミング言語のパフォーマンスのさまざまな側面を実際に比較できるのではなく、かなり洗練されたコード最適化手法の動物園があります。 もちろん、好奇心が強いですが、少し間違っています。 しかし、そこに秩序を回復することは難しくないようです。



さまざまなベンチマークゲームベースの研究については、信じないでください。コードを参照してください。



All Articles