web-dev-qa-db-ja.com

C ++は、JITを使用したJVMまたはCLRよりも高速である可能性があるという主張を裏付けるものは何ですか?

私が多くの質問で気付いたSEで繰り返し発生するテーマは、C++がJavaのような高水準言語よりも高速で効率的であるという継続的な議論です。反対の意見は、JVMなどのおかげで、最近のJVMまたはCLRは、増え続けるタスクに対して同様に効率的であり、C++ 何をしているのか、なぜそうしているのかを知っている場合にのみ、より効率的であるということです。一定の方法でパフォーマンスを向上させることができます。それは明白であり、完全に理にかなっています。

whyおよびhowに関する基本的な説明(そのようなものがある場合)が知りたいのですが、特定のタスクはC++ではJVMまたはCLRより高速です?それは単に、C++がマシンコードにコンパイルされているのに対し、JVMまたはCLRには実行時にJITコンパイルの処理オーバーヘッドがまだあるからですか?

トピックを調査しようとすると、C++がハイパフォーマンスコンピューティングにどのように利用できるかを正確に理解するための詳細な情報なしで、上記で概説したのと同じ議論が見つかります。

120
Anonymous

それはすべてメモリに関するものです(JITではありません)。 JITの「Cに対する利点」は、インライン化による仮想呼び出しまたは非仮想呼び出しの最適化にほとんど制限されています。これは、CPU BTBがすでに一生懸命に取り組んでいることです。

最近のマシンでは、RAM=はreally遅い(CPUが行うものに比べて)遅い)、つまり、可能な限り多くのキャッシュ(使用するメモリが少ないほど簡単です)は 最大100倍高速 しないキャッシュよりも多くなります。JavaはC++よりも多くのメモリを使用し、キャッシュを完全に活用するアプリケーションの作成を困難にします。

  • 各オブジェクトには少なくとも8バイトのメモリオーバーヘッドがあり、プリミティブの代わりにオブジェクトを使用する必要があるか、多くの場所(つまり、標準のコレクション)で優先されます。
  • 文字列は2つのオブジェクトで構成され、 8バイトのオーバーヘッド があります。
  • UTF-16は内部で使用されます。つまり、各ASCII文字には1バイトではなく2バイトが必要です(最近、Oracle JVMは、純粋なASCII =文字列)。
  • 集約参照タイプ(つまり、構造体)はなく、集約参照タイプの配列もありません。 A Javaオブジェクト、またはJavaオブジェクトの配列は、C構造体および配列と比較して、L1/L2キャッシュの局所性が非常に低いです。
  • Javaジェネリックは型消去を使用します。型消去は、型インスタンス化と比較してキャッシュの局所性が低くなります。
  • オブジェクトの割り当ては不透明であり、オブジェクトごとに個別に行う必要があるため、アプリケーションが意図的にデータをキャッシュに適した方法でレイアウトし、それを構造化データとして扱うことは不可能です。

キャッシュに関連しない他のいくつかの要因:

  • スタック割り当てはないため、操作するすべての非プリミティブデータはヒープ上にあり、ガベージコレクションを実行する必要があります(最近のJITの中には、特定のケースでバックグラウンドでスタック割り当てを行うものがあります)。
  • 集約参照タイプがないため、集約参照タイプのスタック渡しはありません。 (ベクター引数の効率的な受け渡しを考えてください)
  • ガベージコレクションはL1/L2キャッシュの内容を損なう可能性があり、GCの世界停止は対話性を損なう。
  • データ型間の変換には常にコピーが必要です。ソケットから取得した一連のバイトへのポインタを取得して、それらを浮動小数点数として解釈することはできません。

これらのいくつかはトレードオフです(手動のメモリ管理を行う必要がないことは多くのパフォーマンスを放棄する価値があります)、いくつかはおそらく結果ですJavaシンプルに保つ試みの一部であり、一部は設計ミスです(おそらく、後から考えると、Javaが作成されたとき、UTF-16は固定長エンコーディングでした)を選択することで、より理解しやすくなります)。

これらのトレードオフの多くは、Java/JVMの場合とC#/ CILの場合とで大きく異なることに注意してください。 .NET CILには、参照型の構造体、スタックの割り当て/受け渡し、構造体のパックされた配列、および型インスタンス化されたジェネリックがあります。

201

それは単にC++がアセンブリ/マシンコードにコンパイルされているのに対し、Java/C#には実行時のJITコンパイルの処理オーバーヘッドがまだあるからですか?

部分的に、しかし一般的に、絶対に素晴らしい最先端のJITコンパイラーを想定すると、適切なC++コードstillは、Java = 2つの主な理由のコード:

1)C++テンプレートは、genericANDefficient。テンプレートは、C++プログラマーにランタイムオーバーヘッドがゼロである非常に便利な抽象化を提供します。 (テンプレートは基本的にコンパイル時のダックタイピングです。)対照的に、Javaジェネリックは基本的に仮想関数です。仮想関数には常にランタイムオーバーヘッドがあり、通常はインライン。

一般に、Java、C#、さらにはCを含むほとんどの言語では、効率と一般性/抽象化のどちらかを選択できます。 C++テンプレートは両方を提供します(コンパイル時間が長くなります)。

2)C++標準では、コンパイルされたC++プログラムのバイナリレイアウトについて多くのことを言う必要がないという事実により、C++コンパイラはJavaコンパイラよりもはるかに余裕があり、より良い最適化が可能になります(デバッグがより困難になる場合もあります。)実際、Java言語仕様の性質上、特定の領域でパフォーマンスが低下します。たとえば、連続した配列を使用することはできません。 Javaのオブジェクト。オブジェクトポインター(参照)の連続した配列のみを持つことができます。つまり、Javaの配列を反復すると、常に間接参照のコスト。ただし、C++の値のセマンティクスは連続配列を有効にします。もう1つの違いは、C++ではオブジェクトをスタックに割り当てることができるのに対し、Javaはそうではないことです。つまり、実際には、ほとんどのC++プログラムはオブジェクトをスタックに割り当てる傾向があるため、割り当てコストは多くの場合ゼロに近いです。

C++が遅れる可能性がある1つの領域Javaは、多くの小さなオブジェクトをヒープに割り当てる必要がある状況です。この場合、Javaのガベージコレクションシステムは、おそらく標準のnewよりもパフォーマンスが向上します。 Java GCは一括割り当て解除を可能にするため、C++ではdeleteです。ただし、C++プログラマは、メモリプールまたはスラブアロケータを使用してこれを補正できますが、Java = Javaランタイムが最適化されていないメモリ割り当てパターンに直面した場合、プログラマは頼りにならない。

また、このトピックの詳細については、 この優れた答え を参照してください。

68
Charles Salvia

他の答え(これまでのところ6つ)は言及し忘れているようですが、これに答えるために私が非常に重要だと考えるのは、C++の非常に基本的な設計哲学の1つで、Stroustrupが1日目から策定して採用したものです。

使用しない分は支払いません。

(特定のパラダイムに強制されるべきではないように)C++を大きく形成する他のいくつかの重要な基本的な設計原則がありますが、使用しないものに対しては支払いませんは、最も重要なものの1つです。


彼の本The Design and Evolution of C++(通常、[D&E]と呼ばれる)で、Stroustrupは彼がC++を思いついたのに何が必要だったかを説明していますそもそも。私自身の言葉で言えば、博士論文(ネットワークシミュレーション、IIRCに関係するもの)について、彼はSIMULAでシステムを実装しました。彼が非常に気に入ったのは、彼の考えをコードで直接表現できる点で言語が非常に優れていたためです。しかし、結果として得られたプログラムの実行速度は遅すぎたため、学位を取得するために、Cの前身であるBCPLで書き直しました。BCPLでコードを書くのは苦痛でしたが、結果として得られたプログラムは、その結果、博士号を取得することができました。

その後、現実世界の問題を可能な限り直接コードに変換できる言語を必要としていましたが、コードを非常に効率的にすることもできました。
それを追求して、彼は後にC++になるものを作成しました。


したがって、上記の目標は、いくつかの基本的な設計原理の1つだけではなく、C++の存在理由に非常に近いものです。そして、それは言語のほぼどこでも見つけることができます:関数はvirtualのみです(仮想関数の呼び出しにはわずかなオーバーヘッドが伴うため)。これは、明示的に要求した場合にのみ自動的に初期化されます。例外のみ実際にそれらをスローするとパフォーマンスが低下します(スタックフレームのセットアップ/クリーンアップを非常に安価にできるようにする明示的な設計目標でしたが)。

C++は、パフォーマンスと引き換えにいくつかの便利さ(「このメソッドをここで仮想化する必要がありますか?」)を与えないことを明示的に選択しました(「いいえ、ありません。コンパイラーはinlineと全体を最適化します!」)、そして当然のことながら、これは確かに、より便利な言語と比較してパフォーマンスが向上しました。

46
sbi

そのトピックについて Googleリサーチペーパー を知っていますか?

結論から:

パフォーマンスに関しては、C++が大差で勝っています。ただし、最も広範なチューニング作業も必要でした。その多くは、平均的なプログラマーには利用できない高度なレベルで行われました。

これは、「実際のC++コンパイラは、Javaコンパイラの経験的測定よりも)高速なコードを生成する」という意味で、少なくとも部分的には説明です。

29
Doc Brown

これはあなたの質問の複製ではありませんが、受け入れられた回答があなたの質問のほとんどに答えます: Javaの最新のレビュー

総括する:

基本的に、Javaのセマンティクスは、C++よりも遅い言語であることを示しています。

したがって、C++を比較する他の言語によっては、同じ答えが得られる場合と得られない場合があります。

C++では次のようになります。

  • スマートなインライン化を行う能力、
  • 局所性の強い一般的なコード生成(テンプレート)
  • 可能な限り小さくコンパクトなデータ
  • 間接を回避する機会
  • 予測可能なメモリ動作
  • 高レベルの抽象化(テンプレート)を使用しているためにのみコンパイラの最適化が可能

これらは言語定義の機能または副作用であり、理論的には、次のような言語よりもメモリと速度を理論的に効率的にします。

  • インダイレクションを大量に使用する(「すべてがマネージドリファレンス/ポインター」言語です):インダイレクションは、CPUが必要なデータを取得するためにメモリにジャンプする必要があることを意味し、CPUキャッシュエラーを増加させます。 C++のように小さなデータが含まれる場合でも、多くの場合
  • メンバーが間接的にアクセスされる大きなサイズのオブジェクトを生成します。これはデフォルトで参照があるためです。メンバーはポインターであるため、メンバーを取得すると、親オブジェクトのコアに近いデータを取得できず、再びキャッシュミスがトリガーされます。
  • ガーバージコレクターを使用する:パフォーマンスの予測可能性を(設計上)不可能にするだけです。

C++の積極的なコンパイラのインライン展開により、多くの間接参照が削減または排除されます。コンパクトなデータの小さなセットを生成する能力により、これらのデータを一緒にパックするのではなく、メモリ全体に分散させない場合、キャッシュフレンドリーになります(どちらも可能です。C++で選択できます)。 RAIIはC++メモリの動作を予測可能にし、高速を必要とするリアルタイムまたはセミリアルタイムシミュレーションの場合の多くの問題を排除します。局所性の問題は、一般にこれによって要約できます。プログラム/データが小さいほど、実行は速くなります。 C++は、データが必要な場所(プール、配列など)にあり、コンパクトであることを確認するためのさまざまな方法を提供します。

明らかに、同じことができる他の言語もありますが、C++ほど多くの抽象化ツールを提供していないため、あまり人気がなく、多くの場合、あまり有用ではありません。

23
Klaim

これは主にメモリ(Michael Borgwardtによる)に関するもので、JITの非効率性が少し追加されています。

言及されていないことの1つはキャッシュです。キャッシュを完全に使用するには、データを連続して(つまり、すべてまとめて)配置する必要があります。これでGCシステムでは、メモリがGCヒープに割り当てられます。これは高速ですが、メモリが使用されると、GCは定期的に起動し、使用されなくなったブロックを削除して、残りをまとめて圧縮します。これらの使用済みブロックを一緒に移動する際の明らかに遅いことは別として、これは、使用しているデータが一緒にスタックされない可能性があることを意味します。 1000個の要素の配列がある場合、一度にすべてを割り当てない限り(そして、新しい要素を削除して作成するのではなく、内容を更新します-ヒープの最後に作成されます)、これらはヒープ全体に散らばります。したがって、それらすべてをCPUキャッシュに読み込むには、いくつかのメモリヒットが必要です。 C/C++アプリはこれらの要素にメモリを割り当てる可能性が高く、ブロックをデータで更新します。 (OK、GCメモリ割り当てのように動作するリストのようなデータ構造がありますが、人々はこれらがベクトルより遅いことを知っています)。

StringBuilderオブジェクトをStringに置き換えるだけで、この動作を確認できます。Stringbuilderは、メモリを事前に割り当てて埋めることで機能します。これは、Java/.NETシステムの既知のパフォーマンストリックです。

「古いコピーを削除して新しいコピーを割り当てる」パラダイムはJava/C#で非常に頻繁に使用されていることを忘れないでください。GCによりメモリの割り当てが非常に高速であると人々に言われているため、分散メモリモデルがあらゆる場所で使用されています(もちろん、文字列ビルダーを除いて)すべてのライブラリはメモリを浪費する傾向があり、多くのメモリを使用しますが、どれも連続性の利点を得ません。これについてGCの周りの誇大広告を非難します-彼らはあなたに記憶が自由だったと言った、笑.

GC自体は明らかに別のパフォーマンスヒットです。実行すると、ヒープ全体をスイープするだけでなく、未使用のブロックをすべて解放し、ファイナライザーを実行する必要があります(これは以前は個別に行われていましたが)アプリが停止した次のラウンド)(それがまだそのようなパフォーマンスのヒットであるかどうかはわかりませんが、私が読んだすべてのドキュメントでは、本当に必要な場合にのみファイナライザーを使用するように言われています)、その後、それらのブロックを適切な位置に移動して、ヒープが圧縮し、ブロックの新しい場所への参照を更新します。ご覧のとおり、大変な作業です。

C++メモリのパフォーマンスヒットはメモリ割り当てに帰着します。新しいブロックが必要な場合は、ヒープを歩いて次の十分な大きさの空き領域を探す必要があります。ヒープは断片化が激しく、GCほど高速ではありません。 「最後に別のブロックを割り当てる」だけですが、GC圧縮が行うすべての作業ほど遅くはないと思います。複数の固定サイズのブロックヒープ(別の方法としてはメモリプールとして知られています)を使用することで軽減できます。

セキュリティチェックが必要なGACからのアセンブリの読み込み、プローブパス( sxstrace をオンにして、それが何になっているのかを確認してください!)、および他の一般的なオーバーエンジニアリングが多いようです。 C/C++よりもJava/.netの方が人気があります。

7
gbjbaanb

「それは単にC++がアセンブリ/マシンコードにコンパイルされるのに対し、Java/C#は実行時のJITコンパイルの処理オーバーヘッドをまだ持っているからですか?」基本的に、はい!

ただし、Javaは、JITコンパイルよりもオーバーヘッドが多くなります。たとえば、ArrayIndexOutOfBoundsExceptionsNullPointerExceptions)。ガベージコレクタは、もう1つの重要なオーバーヘッドです。

かなり詳細な比較 ここ があります。

6
vaughandroid

以下はネイティブコンパイルとJITコンパイルの違いのみを比較するものであり、特定の言語やフレームワークの詳細はカバーしていないことに注意してください。これ以外に特定のプラットフォームを選択する正当な理由があるかもしれません。

ネイティブコードの方が速いと主張するときは、ネイティブコンパイルコードとJITコンパイルコードの通常の使用例について話します。JITコンパイルアプリケーションの一般的な使用はユーザーが実行することになります。即時の結果(例:最初にコンパイラーで待機しない)。その場合、JITでコンパイルされたコードはネイティブコードに匹敵するか、それを上回ることができると誰もが真っ直ぐに主張できるとは思いません。

ある言語Xで書かれたプログラムがあり、それをネイティブコンパイラーで、さらにJITコンパイラーでコンパイルできるとします。各ワークフローには、同じ段階が含まれています。これは、(コード->中間表現->マシンコード->実行)として一般化できます。 toと2の大きな違いは、ユーザーに表示されるステージとプログラマーに表示されるステージです。ネイティブコンパイルでは、プログラマーは実行ステージ以外のすべてを見ますが、JITソリューションでは、マシンコードへのコンパイルが実行に加えてユーザーに表示されます。

AはBより速いという主張は、プログラムの実行にかかる時間を示していますユーザーから見た場合。実行段階で両方のコードが同じように実行されると仮定する場合、JITワークフローは、マシンコードへのコンパイルの時間T(T> 0)も確認する必要があるため、ユーザーにとって低速であると想定する必要があります。 、JITワークフローがネイティブワークフローと同じように実行される可能性があるため、ユーザーはコードの実行時間を短縮する必要があります。これにより、実行+マシンコードへのコンパイルが実行ステージのみよりも短くなります。ネイティブワークフローの。つまり、JITコンパイルでは、ネイティブコンパイルよりもコードを最適化する必要があります。

ただし、実行を高速化するために必要な最適化を実行するには、マシンコードステージへのコンパイルにより多くの時間を費やす必要があるため、これはかなり実行不可能であり、最適化されたコードの結果として保存した時間は、実際には失われます。それをコンパイルに追加します。言い換えると、JITベースのソリューションの「遅さ」は、JITコンパイルの追加時間のためだけではなく、そのコンパイルによって生成されたコードのパフォーマンスは、ネイティブソリューションよりも遅くなります。

例を使用します:レジスター割り当て。メモリアクセスはレジスタアクセスよりも数千倍遅いため、できる限りレジスタを使用し、メモリアクセスをできる限り少なくしたいのですが、レジスタの数は限られており、必要なときに状態をメモリにスピルする必要があります。レジスタ。計算に200ミリ秒かかるレジスタ割り当てアルゴリズムを使用すると、実行時間が2ミリ秒短縮されます-JITコンパイラの時間を最大限に活用していません。高度に最適化されたコードを生成できるChaitinのアルゴリズムのようなソリューションは不適切です。

JITコンパイラーの役割は、コンパイル時間と生成されたコードの品質の間で最良のバランスを取ることですが、ユーザーを待たせたくないので、高速コンパイル時間に大きな偏りがあります。 JITの場合、実行中のコードのパフォーマンスは遅くなります。これは、ネイティブコンパイラーがコードを最適化するときに時間に拘束されないため、最良のアルゴリズムを自由に使用できるためです。 JITコンパイラの全体的なコンパイル+実行が、ネイティブにコンパイルされたコードの実行時間のみに勝る可能性は事実上0です。

しかし、私たちのVMは単にJITコンパイルに限定されていません。事前コンパイル技術、キャッシング、ホットスワップ、および適応最適化を採用しています。それでは、パフォーマンスはユーザーに表示されるものであるという私たちの主張を修正して、プログラムの実行にかかる時間に制限します(AOTをコンパイルしたと仮定します)。実行中のコードをネイティブコンパイラと効果的に同等にすることができます(またはおそらくそれよりも優れていますか?)。 VMの大きな主張は、特定の関数が実行される頻度など、実行中のプロセスの情報にアクセスできるため、ネイティブコンパイラよりも高品質のコードを生成できる可能性があるということです。 VMは、ホットスワップを介して最も重要なコードに適応最適化を適用できます。

ただし、この引数には問題があります。プロファイルに基づく最適化などはVMに固有のものであると想定されていますが、これは正しくありません。ネイティブコンパイルにも適用できます。プロファイリングを有効にしてアプリケーションをコンパイルし、情報を記録してから、そのプロファイルを使用してアプリケーションを再コンパイルします。また、コードのホットスワップはJITコンパイラのみが実行できるものではなく、ネイティブコードに対しても実行できることも指摘しておく必要があります。これを行うためのJITベースのソリューションは、より簡単に利用でき、開発者にとってはるかに簡単です。したがって、大きな問題は次のとおりです。VMは、ネイティブコンパイルでは提供できない情報を提供して、コードのパフォーマンスを向上させることができますか?

自分では見えません。プロセスはより複雑ですが、典​​型的なVMのテクニックのほとんどをネイティブコードにも適用できます。同様に、AOTコンパイルまたは適応最適化を使用するVMにネイティブコンパイラーの最適化を適用できます。実際には、ネイティブで実行されるコードとVMで実行されるコードの違いは、私たちが信じているほど大きくはありません。最終的には同じ結果が得られますが、そこに到達するには別のアプローチをとります。 VMは反復アプローチを使用して最適化されたコードを生成します。ネイティブコンパイラは最初からそれを期待します(反復アプローチで改善できます)。

C++プログラマーは、get-goからの最適化が必要であり、VMがその方法を理解するのを待っているべきではないと主張するかもしれません。 VMの現在の最適化レベルは、ネイティブコンパイラーが提供できる最適化レベルよりも低いため、これはおそらく私たちの現在のテクノロジーで有効なポイントです。

2
Mark H

この記事 は、c ++とc#の速度を比較しようとする一連のブログ投稿と、高性能のコードを取得するために両方の言語で克服しなければならない問題の概要です。要約は「ライブラリは何よりも重要ですが、C++を使用している場合はそれを克服できます」です。または「現代の言語はより優れたライブラリを備えているため、哲学的な傾斜に応じて、より少ない労力でより速い結果を得ることができます」。

0
Jeff Gates

ここでの本当の質問は「どちらが速いのか」ではないと思います。しかし、「より高いパフォーマンスの可能性が最も高いのはどれですか?」です。これらの用語で見ると、C++は明らかに勝っています。ネイティブコードにコンパイルされ、JITがなく、抽象化のレベルが低いなどです。

それは完全な話からはほど遠いです。

C++はコンパイルされるため、コンパイラーの最適化はコンパイル時に行う必要があり、1つのマシンに適したコンパイラーの最適化は別のマシンでは完全に間違っている可能性があります。また、グローバルコンパイラの最適化では、特定のアルゴリズムやコードパターンが他のアルゴリズムよりも優先される可能性があり、そうなる場合もあります。

一方、JITされたプログラムはJIT時に最適化されるため、プリコンパイルされたプログラムではできないトリックを引き出し、実際に実行されているマシンと実際に実行されているコードに対して非常に具体的な最適化を行うことができます。 JITの初期オーバーヘッドを超えると、場合によっては高速になる可能性があります。

どちらの場合も、アルゴリズムの賢明な実装と、愚かではないプログラマーの他のインスタンスは、はるかに重要な要素になる可能性があります。たとえば、C++で完全に頭の悪い文字列コードを書くことは完全に可能であり、解釈されたスクリプト言語。

0
Maximus Minimus