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


13

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


この答えに関するコメントを読んでいたこの引用を見ました。

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

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

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

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

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

だから私の質問はこれのどれでも本当に真実でありえ、もしそうなら、なぜJavaのヒープ割り当てがそんなに速くなるのかということです。


あなたはSOに関する有用な/関連する以上の同様の質問に対する私の答えを見つけるかもしれません。
ダニエル・プライデン

1
些細なことです。Java(またはその他の管理された制限された環境)を使用すると、オブジェクトを移動し、それらへのポインターを更新できます。C ++と、制御されていないビットキャストを使用したポインター演算により、すべてのオブジェクトが永久にその場所に固定されます。
SKロジック

3
Javaメモリ管理は常にメモリをコピーするため、Javaメモリ管理の方が速いと誰かが言うのを聞くとは思いませんでした。はぁ。
gbjbaanb

1
@gbjbaanb、メモリ階層について聞いたことがありますか?キャッシュミスペナルティ?汎用アロケーターは高価ですが、第1世代のアロケーターは1つの加算操作にすぎないことを理解していますか?
SKロジック

1
これはいくつかのケースでは多少当てはまりますが、javaではヒープにすべてを割り当て、c ++ではスタックに大量のオブジェクトを割り当てるという点を見逃しています。
JohnB

回答:


23

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

全体として、JVMガベージコレクターは非常に適切に設計され、非常に効率的であると言っても過言ではないと思います。それはおそらく最高の汎用ですメモリ管理システムです。

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

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

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

ガベージコレクションには、パフォーマンスの観点からいくつかの重要な利点もあります。

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

Java GCには大きな欠点が1つあります。ガベージコレクションの作業は、定期的な作業の塊で延期され、行われるため、ガベージコレクションのためにGCの一時停止が発生し、レイテンシに影響する可能性があります。通常、これは一般的なアプリケーションでは問題になりませんが、ハードリアルタイムが必要状況(ロボット制御など)ではJavaを除外できます。通常、ソフトリアルタイム(ゲーム、マルチメディアなど)は問題ありません。


c ++領域には、この問題に対処する専用のライブラリがあります。おそらく最も有名な例はSmartHeapです。
トビアスラングナー

5
ソフトリアルタイムは、通常停止しても問題ないという意味ではありません。それは、停止/クラッシュ/失敗の代わりに、実際には悪い状況(通常は予想外)で一時停止/再試行できることを意味します。通常、一時停止する音楽プレーヤーを使用したい人はいません。GCの一時停止の問題は、通常予期せずに発生することです。そのように、GCの一時停止は、ソフトリアルタイムアプリケーションでも受け入れられません。GCの一時停止は、ユーザーがアプリケーションの品質を気にしない場合にのみ許容されます。そして今、人々はもはやそれほど素朴ではありません。
エオニル

1
あなたの主張を裏付けるために、いくつかのパフォーマンス測定値を投稿してください。
JBRウィルキンソン

1
現実に@Demetriしかし、その場合にのみ場合は(さえ予測できない、何度も!)あまり起きあなたには、いくつかの非現実的な制約を満たすことができる場合を除きます。言い換えれば、C ++はリアルタイムの状況でははるかに簡単です。
エオニル

1
完全を期すために、GCのパフォーマンスには別の欠点があります。既存のGCのほとんどは、別のコアで実行される可能性のある別のスレッドでメモリを解放するため、同期のためにGCに重大なキャッシュ無効化コストが発生することを意味します異なるコア間のL1 / L2キャッシュ。さらに、主にNUMAであるサーバーでは、L3キャッシュも同期する必要があります(Hypertransport / QPIを介して、ouch(!))。
いいえバグうさぎ

3

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

視覚的なアナロジーの1つに、カーペットが敷かれたアパート(住宅ユニット)が与えられます。カーペットが汚れています。アパートの床をきれいにするための最速の方法(時間の面で)は何ですか?

答え:古いカーペットを巻くだけです。捨てる; 新しいカーペットを展開します。

ここで何を怠っていますか?

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

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

副次的な問題として、TCMallocとして知られるC / C ++割り当てマネージャーとオブジェクト参照カウントは、理論的にはGCシステムの最高のパフォーマンス要求を満たすことができます。


実際にはc ++ 11にはガベージコレクションABIさえあります。これは、SOで得たいくつかの回答とかなり似ています
aaronman

既存のC / C ++プログラム(Linuxカーネルなどのコードベースやlibtiffなどのarchaic_but_still_economically_importantライブラリ)を破壊することは、C ++の言語革新の進展を妨げる恐れがあります。
-rwong

理にかなっていますが、c ++ 17ではより完全になると思いますが、真実はc ++でプログラミングする方法を実際に学んだ後はもうそれさえ望まないでしょう、おそらく2つのイディオムを組み合わせる方法を見つけることができますうまく
アーロンマン

世界を止めないガベージコレクターがあることに気付いていますか?コンパクト化(GC側)とヒープフラグメンテーション(汎用C ++アロケーター)のパフォーマンスへの影響を考慮しましたか?
SKロジック

2
この類推の主な欠点は、GCが実際に行うことは、汚れたビットを見つけ、それらを切り取り、残りのビットを一緒に見て新しいカーペットを作成することであると思います。
svick

3

主な理由は、Javaに新しいメモリの塊を要求すると、ヒープの最後までまっすぐに進み、ブロックを提供するためです。このように、メモリの割り当ては、スタックでの割り当てと同じくらい高速です(C / C ++ではほとんどの場合これを行いますが、それ以外は..)

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

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

現在のコレクションは、すべてのオブジェクトがローカルに割り当てられている場合、たとえば単一の連続したブロックにある場合、高速になります。さて、Javaでは、オブジェクトはヒープの自由端から1つずつ割り当てられるため、連続したブロックを取得することはありません。幸運なことに(つまり、GCコンパクションルーチンの気まぐれとオブジェクトのコピー方法まで)、それらは幸福に隣接することになります。一方、C / C ++は(明らかにスタックを介して)連続割り当てを明示的にサポートします。一般に、C / C ++のヒープオブジェクトは、JavaのBTWと違いはありません。

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


2
GCをまったく理解していないようです。最も典型的なシナリオを考えてみましょう-数百の小さなオブジェクトが絶えず割り当てられていますが、1秒間以上生き残るのはわずか数十オブジェクトです。この方法では、メモリを解放するためのコストはまったくありません-この数十は若い世代からコピーされ(追加の利点としてコンパクト化されます)、残りは無料で破棄されます。ちなみに、哀れなDalvik GCは、適切なJVM実装で見られる最新の最先端GCとは何の関係もありません。
SKロジック

1
これらの解放されたオブジェクトの1つがヒープの中央にある場合、残りのヒープは圧縮されてスペースが再利用されます。それとも、あなたが説明するベストケースでない限り、GCの圧縮は行われないと言っていますか?ここでは、後の世代の途中でオブジェクトをリリースしない限り、世代別GCの方がはるかに優れていることを知っています。その場合、影響は比較的大きくなります。世代別GCを作成するときのGCのトレードオフについて説明した、GCに取り組んでいるMicrosoftieによって書かれたものがありました。
gbjbaanb

1
何の「ヒープ」について話しているのですか?ガベージの大部分は若い世代の段階で回収され、パフォーマンスの利点のほとんどはまさにそのコンパクト化によるものです。もちろん、それは関数型プログラミングに典型的なメモリ割り当てプロファイル(多くの短命の小さなオブジェクト)でほとんど見られます。そして、もちろん、まだ十分に検討されていない最適化の機会が数多くあります。たとえば、特定のパスのヒープ割り当てをスタックまたはプール割り当てに自動的に変換する動的領域分析などです。
SKロジック

3
ヒープ割り当ては、スレッドの同期を必要とし、スタックが(定義により)しません-私は「速いスタックとしてとして」ヒープ割り当てがあることをあなたの主張に反対
JBRWilkinsonを

1
そうだと思いますが、Javaと.netを使えば次の空きブロックを見つけるためにヒープを歩く必要がないので、その点で大幅に高速になりますが、そうです-そうです、そうする必要がありますロックされているため、スレッド化されたアプリが破損します。
gbjbaanb

2

エデンスペース

だから私の質問はこれのどれでも本当に真実でありえ、もしそうなら、なぜJavaのヒープ割り当てがそんなに速くなるのかということです。

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

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

これは、アルゴリズムやmallocCでの汎用目的operator new、C ++ でのスローよりも、強制的にページフォールトを減らすという点で非常に高速です。

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

また、要素を削除できるデータ構造にも使用できますが、個別に割り当てを解除できないため、これらの要素は実際にはメモリから解放されません。シーケンシャルアロケーターを使用するこのような構造は、別のシーケンシャルアロケーターを使用してデータを新しい圧縮コピーにコピーする遅延パスがない限り、メモリを消費しますなんらかの理由で行うことはありません-データ構造の新しいコピーを順番に割り当てて、古い構造のすべてのメモリをダンプするだけです)。

コレクション

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

そのため、代わりに、1回のGCサイクルの後、「Eden」スペース内の既存のオブジェクト(連続的に割り当てられた)を通過し、まだ​​参照されているオブジェクトは、個々のチャンクを解放できるより汎用的なアロケーターを使用して割り当てられます。参照されなくなったものは、パージの過程で単純に割り当て解除されます。したがって、基本的には、「まだ参照されている場合はEdenスペースからオブジェクトをコピーし、パージします」。

通常、これは非常に高価になるため、元々すべてのメモリを割り当てていたスレッドが大幅に停止することを避けるために、別のバックグラウンドスレッドで行われます。

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

速度

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

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

これが主な違いであり、JavaがCまたはC ++よりも単純な呼び出しを使用するmallocoperator new、多数の小さなチャンクを個別に割り当てることで、CまたはC ++を非常に上回ります。もちろん、通常、GCサイクルが開始されると、アトミック操作とロックの可能性がいくつかありますが、おそらくかなり最適化されています。

基本的に、簡単な説明は、シングルスレッドでより重いコストを支払うこと(malloc)とシングルスレッドでより安いコストを支払い、次に並行して実行できる別のスレッドでより重いコストを支払うこと(GC)に要約されます。この方法の欠点は、アロケータが既存のオブジェクト参照を無効にせずにメモリをコピー/移動できるようにするために、オブジェクト参照からオブジェクトに取得するために2つのインダイレクションが必要であることを意味します「エデン」スペースから移動しました。

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


0

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

割り当て/割り当て解除だけを見ると、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に最適化されているため、マルチスレッド環境では共有メモリ変数はありません多くの場合に使用されます(ロックまたは共有メモリ変数は非常に高価です)。

第三に、ガベージコレクションなしで必要になるかもしれない参照カウントのようなものがあり、それは無料ではありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.