質問: Java が C++ よりもはるかに遅いのはどのような状況ですか?
回答: Ben Maurer:
この質問に答えるには、まず問題を考えられる速度低下の原因としていくつかに分ける必要があります:
ガベージコレクター。これは「両刃の剣」です。プログラムが「ほとんどのオブジェクトは若い世代で消滅する」モデルに従っている場合、ガベージ コレクターは非常に有益です (断片化が少なく、キャッシュの局所性が向上します)。ただし、プログラムがこのモデルに従っていない場合、JVM はヒープ メモリの再利用に多くのリソースを費やすことになります。
大きな物体。 Java では、すべてのオブジェクトに vtable ポインターがありますが、C++ では、POD 構造を使用する際の追加のオーバーヘッドはありません。さらに、すべての Java オブジェクトをロックできます。その実装は JVM に依存しており、オブジェクトにフィールドを追加する必要がある場合があります。大きなオブジェクト == キャッシュするオブジェクトの数が少ない == 速度が遅い。 (一方、Java 7 は圧縮ポインターに 64 ビット レコードを使用します。これが問題の一部です。
インライン オブジェクトの欠如。Java では、すべてのクラスがポインターです。C++ では、オブジェクトは他のオブジェクトを一緒に割り当てることができます。これにより、キャッシュの局所性が向上し、Java でクライアント C++ コードを呼び出す必要がある場合に発生する動的メモリ割り当てのオーバーヘッドが軽減されます。たとえば、Java で XML パーサーを作成する場合、(char[] を使用せずに) String オブジェクトのみを使用すると、追加のスペースを割り当てる必要があるため、処理が遅くなります。 JVM では、ほとんどすべての関数呼び出しが仮想関数呼び出しであるため、多くの場合、JVM ではこの問題を解決できず、コードのインライン化が妨げられ、コードが遅くなります。高度なコンパイル機能とアセンブリに変換する機能が欠けています。アセンブリの恩恵を受けるコードは Java ではうまく動作しない可能性があります
私の意見では、最大の問題はガベージ コレクションであり、これは Java と C++ の間で複数のコードを強制する場合に最も一般的な問題です。さらに、プログラムのワーキング セットが L2 キャッシュの外側に配置されている場合、大きなオブジェクトやインライン オブジェクトの欠如などの問題も大きな違いにつながります。非効率な強制抽象化やプラットフォーム関数も速度低下を引き起こす可能性がありますが、これは通常、低レベルのコードが原因で発生するだけであり、適切に作成された Java コード ベースを使用している場合には、通常は大きな問題にはなりません。基本的には、Ben Maurer (おい、Ben!) の答えに同意しますが、いくつかの小さな違いがあります:
最新の JVM では、この割り当てが (a) ローカル関数または (b) ローカル関数から実行されない場合、スレッドがエスケープするときにエスケープします。分析は、固定割り当てを効果的に決定できます。つまり、割り当てがロックを必要としない場合、通常、どちらの場合も、それは単純な「ポインターをバンプする」割り当てです。
翻訳者注:
エスケープ分析とは、オブジェクト ポインターが複数のメソッドまたはスレッドによって参照される場合、ポインターのダイナミック レンジを分析する方法を指すコンパイル最適化テクノロジです。ポインタがエスケープします。
ポインタの衝突 (ポイントの衝突) Java ヒープ内のメモリは完全に規則的であり、使用されているメモリはすべて片側に配置され、空きメモリは反対側に配置されると仮定します。分割点の指標としてポインタを中央に配置し、空き領域にポインタを移動させるだけです。この割り当て方法を「ポインタ衝突」と呼びます。
エスケープ解析がなくても、若い世代の割り当てはポインター衝突を通じてスレッド ローカル割り当てバッファー (TLAB) で行われ、同期は必要ありません。したがって、Java での小さなオブジェクトの割り当ては、C 言語で実装された malloc() メソッドよりも高速な場合があります。 Google の tcmalloc などのより優れた malloc メソッドも同様のアプローチを採用しています。ただし、C 言語はメモリ上に割り当てられたオブジェクトを再割り当てできないため、いくつかの点で制限されます。
インライン化と仮想関数には問題がありますが、実際には、Java は場合によっては C よりも優れたパフォーマンスを発揮することさえあります。特に、インライン化は実行時ではなくコンパイル時に行われるため、C では動的リンクを介してインライン化を実装できません。 Java は、クラスの実際の実装がコンパイル時に利用できない場合でも、さまざまなクラスまたはライブラリの境界を越えて関数を動的にインライン化できます。多くのジョブでは、このアプローチは、常に仮想テーブルの呼び出しを必要とする C++ 仮想関数呼び出しよりも効率的です。 JIT コンパイラーは、以前の動的属性が失われた場合 (新しいクラスがロードされた場合など)、インライン最適化をインテリジェントにキャンセルできます。
GCC の新しいバージョンは、「プログラム全体の最適化」または「リンク時の最適化」と呼ばれる、この領域でのいくつかの最適化を提供します。これにより、プロジェクト スコープ内のオブジェクト ファイル全体のインライン化が可能になります。ただし、動的リンクによるインライン化(インライン化による zlib の呼び出しなど)の実装は基本的に禁止されています。多くの大規模プロジェクトは、標準ライブラリの機能をコードにコピーすることによって実装されます。