数百万の小さな一時オブジェクトを作成するためのベストプラクティス


109

何百万もの小さなオブジェクトを作成(およびリリース)するための「ベストプラクティス」とは何ですか?

私はJavaでチェスプログラムを書いており、検索アルゴリズムは可能な各移動に対して単一の「移動」オブジェクトを生成し、名目検索は1秒あたり100万以上の移動オブジェクトを簡単に生成できます。JVM GCは私の開発システムの負荷を処理することができましたが、次のような代替アプローチを模索しています。

  1. ガベージコレクションのオーバーヘッドを最小限に抑える。
  2. ローエンドシステムのピークメモリフットプリントを削減します。

オブジェクトの大部分は存続期間が非常に短いですが、生成された移動の約1%が永続化され、永続化された値として返されるため、プーリングまたはキャッシングテクニックでは、特定のオブジェクトの再利用を除外する機能を提供する必要があります。

私は完全に具体化されたサンプルコードを期待していませんが、さらに読んだり研究したりするための提案、または同様の性質のオープンソースの例をいただければ幸いです。


11
Flyweightパターンはあなたのケースに適していますか?en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland

4
それをオブジェクトにカプセル化する必要がありますか?
nhahtdh 2013年

1
オブジェクトは重要な共通データを共有しないため、Flyweightパターンは適切ではありません。オブジェクト内のデータのカプセル化に関しては、プリミティブにパックするには大きすぎるため、POJOの代替を探しています。
謙虚なプログラマ

回答:


47

詳細なガベージコレクションでアプリケーションを実行します。

java -verbose:gc

そして、それは収集するときに教えてくれます。掃引には、高速掃引と完全掃引の2つのタイプがあります。

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

矢印はサイズの前後です。

完全なGCではなく、GCを実行している限り、安全です。通常のGCは「若い世代」のコピーコレクターであるため、参照されなくなったオブジェクトは単に忘れられるだけであり、これはまさに望んでいることです。

Java SE 6 HotSpot Virtual Machine Garbage Collection Tuningを読むと役立つでしょう。


Javaヒープサイズを試して、完全なガベージコレクションがまれであるポイントを見つけてください。Java 7では、新しいG1 GCが高速な場合があります(また、低速な場合もあります)。
Michael Shopsin 2013年

21

バージョン6以降、JVMのサーバーモードはエスケープ分析手法を採用しています。これを使用すると、GCをすべて一緒に回避できます。


1
エスケープ分析は失望することがよくあります。JVMが実行中の処理を理解しているかどうかを確認することは価値があります。
日産ワカルト2013年

2
これらのオプションの使用経験がある場合:-XX:+ PrintEscapeAnalysisおよび-XX:+ PrintEliminateAllocations。共有していただければ幸いです。私はそうしないので、正直に言って。
ミハイル

stackoverflow.com/questions/9032519/…を参照してください。JDK7のデバッグビルドを取得する必要があります。私はそれを行っていませんが、JDK 6では成功しています。
Nitsan Wakart 2013年

19

さて、ここにはいくつかの質問があります!

1-存続期間の短いオブジェクトはどのように管理されますか?

前述のように、JVMは弱い世代仮説に従うため、大量の短期間のオブジェクトを完全に処理できます。

メインメモリ(ヒープ)に到達したオブジェクトについて話していることに注意してください。これは常にそうであるとは限りません。作成したオブジェクトの多くは、CPUレジスタを残しさえしません。たとえば、このforループを考えてみましょう

for(int i=0, i<max, i++) {
  // stuff that implies i
}

ループ展開(JVMがコードで頻繁に実行する最適化)について考えないでください。maxがに等しい場合Integer.MAX_VALUE、ループの実行には時間がかかる場合があります。ただし、i変数がループブロックをエスケープすることはありません。したがって、JVMはその変数をCPUレジスタに入れ、定期的にインクリメントしますが、メインメモリに送り返すことはありません。

そのため、何百万ものオブジェクトをローカルでのみ使用する場合、それらを作成することは大したことではありません。それらはエデンに保管される前に死んでいるので、GCはそれらに気付くことさえありません。

2-GCのオーバーヘッドを減らすことは有用ですか?

いつものように、状況によります。

まず、何が起こっているのかを明確に把握できるように、GCロギングを有効にする必要があります。で有効にでき-Xloggc:gc.log -XX:+PrintGCDetailsます。

アプリケーションがGCサイクルで多くの時間を費やしている場合は、はい、GCを調整します。それ以外の場合は、実際には価値がない場合があります。

たとえば、100ミリ秒ごとに10ミリ秒かかる新しいGCがある場合、時間の10%をGCに費やし、1秒あたり10個のコレクションがあります(これはハウウージです)。そのような場合、10のGC /秒がまだ存在するため、GCのチューニングに時間を費やすことはありません。

3-ある程度の経験

特定のクラスを大量に作成しているアプリケーションでも同様の問題がありました。GCログで、アプリケーションの作成速度が約3 GB /秒であることに気づきました(これは、1秒あたり3ギガバイトのデータです!)。

問題:作成されているオブジェクトが多すぎるために頻繁に発生するGCが多すぎる。

私の場合、私はメモリプロファイラーをアタッチし、クラスがすべてのオブジェクトの大きな割合を占めていることに気付きました。インスタンス化を追跡して、このクラスが基本的にオブジェクトにラップされたブール値のペアであることを確認しました。その場合、2つのソリューションが利用可能でした。

  • ブール値のペアを返さないようにアルゴリズムを作り直しますが、代わりに、各ブール値を個別に返す2つのメソッドがあります。

  • オブジェクトがキャッシュされ、4つの異なるインスタンスしかなかったことがわかる

2番目の方法を選択したのは、それがアプリケーションへの影響が最も少なく、導入しやすいためです。スレッドセーフではないキャッシュを備えたファクトリを配置するのに数分かかりました(最終的に4つの異なるインスタンスしかなかったため、スレッドセーフは必要ありませんでした)。

割り当て率は1 GB /秒に低下し、若いGCの頻度も低下しました(3で除算)。

お役に立てば幸いです。


11

値オブジェクトだけがあり(つまり、他のオブジェクトへの参照がない)、本当に大量のオブジェクトを意味する場合はByteBuffers、ネイティブバイトオーダーで直接使用できます(後者は重要です)。数百行が必要です。割り当て/再利用するコード+ゲッター/セッター。ゲッターは似ていますlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

一度だけ、つまり巨大なチャンクを割り当てて、自分でオブジェクトを管理する限り、GCの問題はほぼ完全に解決します。参照の代わりに、渡される必要があるへのインデックス(つまりint)のみByteBufferを指定します。自分でメモリを揃える必要がある場合もあります。

テクニックはを使用するような感じC and void*がしますが、いくつかのラッピングで耐えられます。パフォーマンスの低下は、コンパイラが除去に失敗した場合の境界チェックである可能性があります。主な利点は、ベクトルのようなタプルを処理する場合の局所性です。オブジェクトヘッダーがないため、メモリのフットプリントも減少します。

それ以外の場合、事実上すべてのJVMの若い世代が自明に死に、割り当てコストは単なるポインターバンプであるため、このようなアプローチは必要ない可能性があります。final一部のプラットフォーム(つまり、ARM / Power)でメモリフェンスが必要なフィールドを使用する場合、割り当てコストは少し高くなる可能性がありますが、x86では無料です。


8

GCが問題であると考える場合(他の人はそうではないかもしれないように指摘します)、特別なケース、つまり大量のチャーンを受けるクラスのために独自のメモリ管理を実装します。オブジェクトのプールを試してみると、かなりうまくいくケースを見てきました。オブジェクトプールの実装は十分に踏み込まれたパスであるため、ここに再度アクセスする必要はありません。以下に注意してください。

  • マルチスレッディング:スレッドローカルプールを使用すると、問題が解決する場合があります
  • バッキングデータ構造:削除時に十分に機能し、割り当てオーバーヘッドがないため、ArrayDequeの使用を検討してください
  • プールのサイズを制限します:)

前/後などを測定


6

同様の問題に遭遇しました。まず、小さなオブジェクトのサイズを小さくしてみてください。各オブジェクトインスタンスでそれらを参照するデフォルトのフィールド値をいくつか導入しました。

たとえば、MouseEventにはPointクラスへの参照があります。新しいインスタンスを作成する代わりに、ポイントをキャッシュして参照しました。たとえば、空の文字列でも同じです。

別のソースは、1つのintに置き換えられた複数のブール値であり、ブール値ごとに1バイトのintを使用しました。


ちょうど興味がない:それはあなたのパフォーマンスを賢く購入したのは何ですか?変更の前後にアプリケーションのプロファイルを作成しましたか?その場合、結果はどうでしたか?
アクセル

@Axelオブジェクトはメモリの使用量が少ないため、GCはそれほど頻繁に呼び出されません。確かにアプリのプロファイルを作成しましたが、速度の向上による視覚効果さえありました。
StanislavL 2013年

6

少し前に、このシナリオをいくつかのXML処理コードで扱いました。非常に小さく(通常は文字列のみ)非常に短期間(XPathチェックの失敗は一致しないため、破棄される)数百万のXMLタグオブジェクトを作成していることに気付きました。

私はいくつかの真剣なテストを行い、新しいタグを作成する代わりに、破棄されたタグのリストを使用して速度を約7%向上させることができるだけであるという結論に達しました。ただし、実装すると、フリーキューが大きくなりすぎた場合、それをプルーニングするメカニズムを追加する必要があることがわかりました。これにより、最適化が完全に無効になり、オプションに切り替えました。

要約すると、おそらくそれだけの価値はないでしょうが、あなたがそれについて考えているのを見てうれしいです。


2

あなたがチェスプログラムを書いているとすれば、まともなパフォーマンスのために使用できるいくつかの特別なテクニックがあります。単純なアプローチの1つは、long(またはバイト)の大きな配列を作成し、それをスタックとして扱うことです。ムーブジェネレーターがムーブを作成するたびに、2、3の数値をスタックにプッシュします。たとえば、正方形から移動して正方形に移動します。検索ツリーを評価すると、動きが飛び出し、ボード表現が更新されます。

表現力が必要な場合はオブジェクトを使用します。(この場合は)速度が必要な場合は、ネイティブにしてください。


1

このような検索アルゴリズムで使用した1つの解決策は、Moveオブジェクトを1つだけ作成し、それを新しいmoveで変更してから、スコープを離れる前に移動を元に戻すことです。おそらく、一度に1つの動きだけを分析してから、どこかに最良の動きを保存するだけです。

それが何らかの理由で実行不可能であり、ピーク時のメモリ使用量を減らしたい場合は、メモリ効率に関する優れた記事がここにあります:http : //www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf


リンク切れ。その記事の別の出典はありますか?
ドナウ

0

何百万ものオブジェクトを作成し、適切な方法でコードを記述してください。これらのオブジェクトへの不要な参照を保持しないでください。GCはあなたのために汚い仕事をします。前述のように冗長GCを試して、実際にGCされているかどうかを確認できます。Javaはオブジェクトの作成と解放についてです。:)


1
ごめんなさい、私はあなたのアプローチに同意しません... Javaは、他のプログラミング言語と同様に、OPがGCによって制約されている場合、その制約の範囲内で問題を解決することです。
Nitsan Wakart 2013年

1
私は、Javaが実際にどのように機能するかを彼に話しています。何百万もの一時オブジェクトが存在する状況を回避できない場合、最善のアドバイスは、一時クラスを軽量にし、参照を1ステップではなくできるだけ早く解放することです。何か不足していますか?
gyorgyabraham 2013年

Javaはガベージの作成をサポートしており、それをクリーンアップしてくれます。OPがオブジェクトの作成を回避できず、GCで費やされた時間に不満がある場合、それは悲しい結末です。私が異論を唱えたのは、GCをより適切に動作させるために、Javaを適切に動作させるための推奨事項です。
日産ワカルト2013年

0

Javaでのスタック割り当てとエスケープ分析について読むべきだと思います。

このトピックをさらに掘り下げると、オブジェクトがヒープに割り当てられておらず、ヒープ上のオブジェクトと同じようにGCによって収集されない場合があります。

これはJavaでどのように機能するかの例とともに、エスケープ分析のWikipediaの説明があります。

http://en.wikipedia.org/wiki/Escape_analysis


0

私はGCの大ファンではないので、常にそれを回避する方法を模索しています。この場合、オブジェクトプールパターンを使用することをお勧めします

後で再利用できるように、スタックに格納して新しいオブジェクトを作成しないようにするのが目的です。

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

3
小さなオブジェクトにプールを使用するのは非常に悪い考えです。起動するにはスレッドごとにプールが必要です(または、共有アクセスによりパフォーマンスが低下します)。このようなプールは、優れたガベージコレクタよりもパフォーマンスが低下します。最後に、GCは、同時実行コード/構造を処理する場合に最適です。多くのアルゴリズムは、ABAの問題がないため、実装が非常に簡単です。参照 並行環境でのカウントには、少なくともアトミック操作+メモリフェンス(LOCK ADDまたはCAS on x86)
bestsss

1
プール内のオブジェクトの管理は、ガベージコレクターを実行させるよりもコストがかかる場合があります。
するThorbjörnRavnアンデルセン

@ThorbjørnRavnAndersen一般的に私はあなたに同意しますが、そのような違いを検出することは非常に困難であり、GCがあなたのケースでよりうまく機能するという結論に達した場合、そのような違いが重要であれば非常にユニークなケースでなければなりません。どのようにしても、オブジェクトプールがアプリを保存する可能性があります。
Ilya Gazman

1
私は単にあなたの議論を理解しませんか?GCがオブジェクトプーリングよりも高速であるかどうかを検出することは非常に困難ですか?したがって、オブジェクトプーリングを使用する必要がありますか?JVMは、クリーンなコーディングと存続期間の短いオブジェクト用に最適化されています。これらがこの質問に関するものである場合(OPが1秒間に100万個生成する場合に期待します)、それは、より複雑でエラーが発生しやすいスキームに切り替えるという証明可能な利点がある場合にのみ可能です。これを証明するのが難しい場合は、なぜ煩わしいのでしょう。
するThorbjörnRavnアンデルセン

0

オブジェクトプールは、ヒープ上のオブジェクトの割り当てを大幅に(場合によっては10倍)改善します。しかし、リンクされたリストを使用した上記の実装は、素朴で間違っています。リンクされたリストは、内部構造を管理するためのオブジェクトを作成して、努力を無効にします。オブジェクトの配列を使用するリングバッファはうまく機能します。例では、(移動を管理するチェスプログラム)で、計算されたすべての移動のリストのホルダーオブジェクトにリングバッファーをラップする必要があります。ムーブホルダーオブジェクトの参照のみが渡されます。

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