web-dev-qa-db-ja.com

Java C ++よりも高速なヒープ割り当て

私はすでにこれを投稿しました 質問 on SOそしてそれは大丈夫でした。残念ながらそれは閉じられました(再開するには投票が1つだけ必要です)が誰かがここに投稿することを提案しましたそれはより良い適合なので、以下は文字通り質問のコピーペーストです


私はこれに関するコメントを読んでいました answer と私はこの引用を見ました。

オブジェクトのインスタンス化とオブジェクト指向の機能は、最初から設計されているため、非常に高速に使用できます(多くの場合、C++より高速です)。とコレクションは高速です。標準Javaは、最も最適化されたCコードであっても、この領域の標準C/C++に勝ります。

1人のユーザー(私が追加する可能性がある非常に高い担当者)は、この主張を大胆に擁護し、

  1. Javaのヒープ割り当てはC++よりも優れています

  2. そして、Javaでコレクションを守るこのステートメントを追加しました

    また、Javaコレクションは、主にメモリサブシステムが異なるため、C++コレクションに比べて高速です。

だから私の質問は、これのどれかが本当に本当でありえますか?.

13
aaronman

これは興味深い質問であり、答えは複雑です。

全体として、JVMガベージコレクターは非常によく設計されており、非常に効率的であると言えます。それはおそらく最高です一般的な目的メモリ管理システム。

C++は、特定の目的のために設計された専用メモリアロケータを使用してJVM GCに勝ることができます。例は次のとおりです。

  • フレームごとのメモリアロケータ。定期的な間隔でメモリ領域全体を消去します。これらは、C++ゲームで頻繁に使用されます。たとえば、一時メモリ領域がフレームごとに1回使用され、すぐに破棄されます。
  • 固定サイズのオブジェクトのプールを管理するカスタムアロケーター
  • スタックベースの割り当て(ただし、JVMは escape analysis などを介してさまざまな状況でこれを行うことに注意してください)

もちろん、特殊なメモリアロケータは、定義によって制限されます。通常、オブジェクトのライフサイクルや管理できるオブジェクトのタイプに制限があります。ガベージコレクションははるかに柔軟です。

ガベージコレクションは、パフォーマンスの観点から、重要なadvantagesも提供します。

  • オブジェクトinstantiationは確かに非常に高速です。新しいオブジェクトがメモリ内で順番に割り当てられる方法のため、ポインタの追加は1回で済みます。これは、通常のC++ヒープ割り当てアルゴリズムよりも確実に高速です。
  • あなたはライフサイクル管理コストの必要性を回避します-例えば参照カウントの頻繁なインクリメントおよびデクリメントにより、多くのパフォーマンスオーバーヘッドが追加されるため(通常、GCの代わりに使用される)参照カウント(GCの代替として使用される)は、パフォーマンスの観点から非常に貧弱です。
  • 不変オブジェクトを使用する場合は、構造共有を利用してメモリを節約し、キャッシュ効率を向上させることができます。これは、ScalaやClojureなどのJVMの関数型言語で頻繁に使用されます。共有オブジェクトの存続期間を管理することは非常に難しいため、GCなしでこれを実行することは非常に困難です。 (私がするように)不変性と構造の共有が大規模な同時アプリケーションを構築するための鍵であり、それが間違いなくGCの最大のパフォーマンス上の利点です。
  • すべてのタイプのオブジェクトとそれぞれのライフサイクルが同じガベージコレクションシステムによって管理されている場合は、コピーを回避できます。 C++とは対照的です。C++は、宛先が異なるメモリ管理アプローチを必要とするか、オブジェクトのライフサイクルが異なるため、データの完全なコピーをとる必要があることがよくあります。

Java GCには1つの大きな欠点があります。ガーベッジを収集する作業は据え置かれ、定期的に一連の作業で行われるため、時々GCの一時停止が発生します。ガベージ。レイテンシに影響する可能性があります。これは通常、一般的なアプリケーションの問題ではありませんが、Javaを除外できますhard realtimeが要件(ロボット制御など)である場合)。ソフトリアルタイム(ゲームなど) 、マルチメディア)は通常は問題ありません。

23
mikera

主な理由は、Javaに新しいメモリの塊を要求すると、ヒープの終わりに直接行き、ブロックを与えることです。これらの方法で、メモリ割り当ては同じくらい高速です。スタックへの割り当てとして(C/C++ではほとんどの場合それが行われますが、それ以外は..)

したがって、割り当ては何よりも高速ですが...メモリを解放するコストは考慮されません。後で何も解放しないからといって、それほどコストがかからないわけではなく、GCシステムの場合、コストは「通常の」ヒープ割り当てよりもかなり多くなります。 GCは、すべてのオブジェクトを実行して、それらが生きているかどうかを確認する必要があります。また、オブジェクトを解放し、メモリをコピーしてヒープを圧縮する必要があります。これにより、最後に高速な割り当てが可能になります。メカニズム(またはメモリ不足になると、たとえばC/C++はすべての割り当てでヒープをウォークして、オブジェクトに適合する空き領域の次のブロックを探します)。

これが、Java/.NETベンチマークがこのように優れたパフォーマンスを示す理由の1つですが、実際のアプリケーションはそのようなパフォーマンスを示します。私は自分の電話でアプリを見るだけで済みます-本当に高速で応答性の高いものはallがNDKを使用して記述されているので、私も驚きました。

最近のコレクションは、すべてのオブジェクトがローカルに割り当てられている場合(たとえば、単一の連続したブロック内)に高速である可能性があります。これで、Javaでは、ヒープの空き端から一度に1つずつオブジェクトが割り当てられるため、連続したブロックを取得できません。幸運なことに(つまり、GC圧縮ルーチンの気まぐれとそれがオブジェクトをコピーする方法に)成功した​​場合に限り、それらは幸運にも連続することになります。一方、C/C++は、(スタックを介して)連続した割り当てを明示的にサポートします。一般に、C/C++のヒープオブジェクトは、JavaのBTWと同じです。

C/C++を使用すると、メモリを節約して効率的に使用するように設計されたデフォルトのアロケーターよりも優れた機能を利用できます。アロケータを一連の固定ブロックプールに置き換えることができるため、割り当てるオブジェクトに最適なサイズのブロックをいつでも見つけることができます。ヒープをウォークすることは、空きブロックがどこにあるかを確認するためのビットマップ検索の問題になり、割り当て解除は単にそのビットマップのビットを再設定することです。コストは、固定サイズのブロックに割り当てるときにより多くのメモリを使用するため、4バイトブロックのヒープがあり、16バイトブロック用のヒープなどがあります。

3
gbjbaanb

エデンスペース

だから私の質問は、これのどれかが本当に本当でありえますか?.

Java GCはとても興味深いので、GCがどのように機能するかについて少し勉強してきました。CおよびC++でメモリ割り当て戦略のコレクションを常に拡張しようとしています(試行することに興味があります) C)で同様のものを実装します。これは、実用的な観点から多くのオブジェクトをバースト方式で割り当てる非常に高速な方法ですが、主にマルチスレッドによるものです。

Java GC割り当てが機能する方法は、非常に安価な割り当て戦略を使用して、最初にオブジェクトを「Eden」に割り当てることですスペース。私の知る限り、シーケンシャルプールアロケーターを使用しています。

これは、アルゴリズムの点で非常に高速であり、Cの汎用mallocまたはC++のoperator newをスローするデフォルトよりも強制的なページフォールトを削減します。

ただし、シーケンシャルアロケーターには明らかな弱点があります。可変サイズのチャンクを割り当てることはできますが、個々のチャンクを解放することはできません。整列のためのパディングを使用して、単純な順次方式で割り当てを行うだけで、割り当てたすべてのメモリを一度にパージできます。これらは通常、CおよびC++で、プログラムの起動時に一度だけ構築する必要があり、繰り返し検索されるか新しいキーが追加されるだけの検索ツリーのように、要素の挿入と削除のみが必要なデータ構造を構築するのに役立ちます(キーは削除されません)。

また、要素を削除できるデータ構造でも使用できますが、これらの要素は個別に割り当てを解除できないため、実際にはメモリから解放されません。シーケンシャルアロケーターを使用するこのような構造は、ますます多くのメモリを消費します、ただし、データが新しい圧縮されたコピーにコピーされる遅延パスがなかった場合別のシーケンシャルアロケーターを使用します(固定アロケーターが何らかの理由で機能しない場合は、非常に効果的なテクニックです-データ構造の新しいコピーをシーケンシャルに割り当て、古いもののすべてのメモリをダンプします)。

コレクション

上記のデータ構造/シーケンシャルプールの例のように、Java GCがこの方法で割り当てられただけの場合、それが多くの個々のチャンクのバースト割り当てに対して超高速であるとしても、それは大きな問題になります。それはソフトウェアがシャットダウンするまで何でも解放できます。その時点で、すべてのメモリプールを一度に解放(パージ)できます。

そのため、代わりに、1回のGCサイクルの後、「エデン」空間の既存のオブジェクトを介してパスが作成され(順次割り当てられます)、まだ参照されているオブジェクトは、個々のチャンクを解放できるより汎用的なアロケーターを使用して割り当てられます。参照されなくなったものは、パージのプロセスで単に割り当て解除されます。つまり、基本的には、「オブジェクトがまだ参照されている場合は、オブジェクトをエデン空間からコピーしてからパージする」ということになります。

これは通常非常にコストがかかるため、最初にすべてのメモリを割り当てたスレッドが大幅にストールするのを避けるために、別のバックグラウンドスレッドで行われます。

メモリがEdenスペースからコピーされ、最初のGCサイクル後に個々のチャンクを解放できるこのより高価なスキームを使用して割り当てられると、オブジェクトはより永続的なメモリ領域に移動します。これらの個々のチャンクは、参照されなくなると、後続のGCサイクルで解放されます。

速度

つまり、大まかに言えば、Java GCがストレートヒープ割り当てでCまたはC++よりも優れている可能性があるのは、メモリの割り当てを要求するスレッドで最も安価で完全に汎用化された割り当て戦略を使用しているためです。次に、別のスレッドでmallocをまっすぐにするなど、より一般的なアロケータを使用するときに通常は必要となる、より費用のかかる作業を節約できます。

したがって、概念的には、GCは全体としてより多くの作業を実際に行う必要がありますが、それはスレッド全体に分散されるため、単一のスレッドによって全額が前払いされません。それは、メモリを割り当てるスレッドがそれを非常に安価に行うことを可能にし、次に、個々のオブジェクトが実際に別のスレッドに解放されるように、物事を適切に行うために必要な真の費用を延期します。 CまたはC++では、mallocまたはoperator newを呼び出すときに、同じスレッド内で全額前払いする必要があります。

これが主な違いであり、Javaがmallocまたはoperator newへの単純な呼び出しを使用して小さな塊を個別に割り当てるだけでCまたはC++よりも優れている可能性がある理由もちろん、GCサイクルが開始されると、通常、いくつかのアトミック操作といくつかの潜在的なロックが発生しますが、おそらくかなり最適化されています。

基本的に、単純な説明は、シングルスレッドで重いコストを支払うこと(malloc)と、シングルスレッドで安いコストを支払うことと、並行して実行できる別のスレッドで重いコストを支払うこと(GC)。この方法の欠点は、アロケータが既存のオブジェクト参照を無効にすることなくメモリをコピー/移動できるようにするために、必要に応じてオブジェクト参照からオブジェクトに取得するために2つの間接参照が必要であること、およびオブジェクトメモリがいったん空間的局所性を失う可能性があることを意味します。 「エデン」スペースから移動しました。

最後に重要なことですが、C++コードは通常、オブジェクトのボートロードをヒープに個別に割り当てないため、比較は少し不公平です。まともなC++コードは、隣接するブロックまたはスタック上の多くの要素にメモリを割り当てる傾向があります。無料のストアで小さなオブジェクトのボートロードを一度に1つずつ割り当てる場合、コードはシテです。

2
user204677

これは科学的な主張ではありません。私は単にこの問題について考えるための食べ物を与えています。

視覚的な例の1つはこれです。カーペットが敷かれたアパート(住宅)が与えられます。カーペットが汚れています。アパートの床をキラキラときれいにする最も速い方法は何時間ですか?

回答:古いカーペットを丸めるだけです。捨てる;新しいカーペットを敷きます。

ここで何を無視していますか?

  • 既存の私物を移動してから移動するコスト。
    • これは、ガベージコレクションの「ストップザワールド」コストとして知られています。
  • 新しいカーペットの費用。
    • これは偶然にもRAMにとっては無料です。

ガベージコレクションは大きなトピックであり、Programmers.SEとStackOverflowの両方に多くの質問があります。

副次的な問題として、TCMallocと呼ばれるC/C++アロケーションマネージャーとオブジェクト参照カウントは理論的にはどのGCシステムの最高のパフォーマンス要求にも対応できます。

2
rwong

すべては、誰が速度を測定するか、どの実装の速度を測定するか、そして何を証明したいかによって異なります。そして、彼らは何を比較します。

割り当て/割り当て解除だけを見ると、C++ではmallocへの呼び出しが1,000,000回、free()への呼び出しが1,000,000回になる可能性があります。 Javaでは、new()への1,000,000の呼び出しと、解放できる1,000,000のオブジェクトを見つけるループで実行されるガベージコレクターがあります。ループはfree()呼び出しよりも高速である可能性があります。

一方、malloc/freeは他の時間を改善しており、通常、malloc/freeは別のデータ構造に1ビットを設定するだけで、同じスレッドで発生するmalloc/freeに最適化されているため、マルチスレッド環境では共有メモリ変数はありません多くの場合に使用されます(そして、ロックまたは共有メモリ変数はvery高価です)。

3番目に、ガベージコレクションなしで必要になる可能性のある参照カウントなどがありますが、それは無料ではありません。

0
gnasher729