Javaプログラムを作成するとき、CPUがキャッシュを使用してデータを格納する方法に影響を与えますか?たとえば、頻繁にアクセスされる配列がある場合、それは次の場合に役立ちますか? 1つのキャッシュラインに収まるほど小さい(通常、64ビットマシンでは128バイト)?使用頻度の高いオブジェクトをその制限内に収めると、そのメンバーが使用するメモリが近くにあり、キャッシュにとどまることが期待できますか? ?
背景:圧縮された デジタルツリー を構築しています。これは、C言語の Judy配列 に大きく影響を受けています。ほとんどの場合、ノード圧縮技術を使用していますが、Judy中心的な設計目標としてCPUキャッシュの最適化があり、ノードタイプとそれらを切り替えるヒューリスティックはそれによって大きく影響されます。私もそれらの恩恵を受けるチャンスがあるのだろうかと思っていましたか?
編集:これまでの回答の一般的なアドバイスは、マシンレベルの詳細をマイクロ最適化しようとしないでください。 Javaを使用しているので、マシンから遠く離れています。私は完全に同意するので、質問がまだ理にかなっていると思う理由をよりよく説明するために、いくつかの(うまくいけば)明確なコメントを追加する必要があると感じました。これらは以下のとおりです:
それらが構築されている方法のために、コンピュータが一般的に扱いやすいものがいくつかあります。解凍で追加のCPUサイクルを使用する必要があったとしても、圧縮データ(メモリから)でJavaコードが著しく高速に実行されるのを見てきました。データがディスクに保存されている場合、その理由は明らかです。 、しかしもちろんRAMでも同じ原理です。
さて、コンピュータサイエンスはそれらが何であるかについて多くのことを言う必要があります。たとえば、参照の局所性はCで優れており、Javaでも優れていると思います。それがランタイムの最適化に役立つのであれば、もっと賢いことをするのに役立つでしょう。しかし、それをどのように達成するかは非常に異なる場合があります。 Cでは、メモリ自体のより大きなチャンクを管理し、関連データに隣接するポインターを使用するコードを作成する場合があります。
Javaでは、特定のランタイムによってメモリがどのように管理されるかについて、私はあまり知りません(そして知りたくありません)。ですから、私も最適化をより高いレベルの抽象化に取り入れなければなりません。私の質問は基本的に、どうすればいいですか?参照の局所性について、Javaで取り組んでいる抽象化レベルで「互いに接近する」とはどういう意味ですか?同じオブジェクト?同じタイプ?同じ配列?
一般的に、抽象化レイヤーが比喩的に言えば「物理法則」を変えるとは思いません。 malloc()
を呼び出さなくなったとしても、スペースが不足するたびに配列のサイズを2倍にすることはJavaでも優れた戦略です。
Javaで優れたパフォーマンスを発揮するための鍵は、JITコンパイラーの裏をかくことを試みるのではなく、慣用的なコードを書くことです。コードを記述して、ネイティブの指導レベルでは、足を撃つ可能性が高くなります。
それは、参照の局所性のような一般的な原則が重要ではないということではありません。確かにそうですが、配列などの使用は、パフォーマンスを意識した慣用的なコードであると考えていますが、「トリッキー」ではありません。
HotSpotおよびその他の最適化ランタイムは、特定のプロセッサ用にコードを最適化する方法について非常に賢いです。 (例として、 このディスカッションをチェックしてください。 )私が専門の機械語プログラマーだった場合、Javaではなく機械語を作成します。そうでなければ、専門家よりもコードを最適化するのに良い仕事ができると考えるのは賢明ではありません。
また、特定のCPUに何かを実装するための最良の方法を知っている場合でも、Javaはwrite-once-run-anywhereです。巧妙なトリックで「最適化」するJavaコードは、JITが最適化の機会を認識しにくくする傾向があります。一般的なイディオムに準拠した単純なコードは、オプティマイザーが認識しやすいためです。したがって、最高のJavaテストベッド用のコード。そのコードは別のアーキテクチャでひどく実行されるか、せいぜい将来のJITの拡張機能を利用できない可能性があります。
優れたパフォーマンスが必要な場合は、シンプルに保ちます。 本当に賢い人々のチームはそれを速くするために働いています。
クランチしているデータが主にまたは完全にプリミティブで構成されている場合(数値の問題など)、次のことをお勧めします。
初期化時に固定サイズのプリミティブ配列のフラット構造を割り当て、その中のデータが定期的に圧縮/最適化されていることを確認します(0-> n、nは要素数を指定して可能な最小の最大インデックス)、反復されますforループの使用を超えています。これは、Javaでの連続した割り当てを保証する唯一の方法であり、圧縮はさらに参照の局所性を改善するのに役立ちます。圧縮は、未使用の要素を反復する必要性を減らし、条件の数を減らすので有益です。forループが反復すると、終了が早く発生し、反復が少なくなる=ヒープ内の移動が少なくなる=キャッシュミスの可能性が低くなります。圧縮はそれ自体でオーバーヘッドを作成しますが、必要に応じて、これは定期的に(処理の主要な領域に関して)のみ実行できます。
さらに良いことに、これらの事前に割り当てられた配列の値をinterleaveすることができます。たとえば、2D空間で何千ものエンティティの空間変換を表現していて、それぞれの運動方程式を処理している場合、次のようなタイトなループが発生する可能性があります。
int axIdx, ayIdx, vxIdx, vyIdx, xIdx, yIdx;
//Acceleration, velocity, and displacement for each
//of x and y totals 6 elements per entity.
for (axIdx = 0; axIdx < array.length; axIdx += 6)
{
ayIdx = axIdx+1;
vxIdx = axIdx+2;
vyIdx = axIdx+3;
xIdx = axIdx+4;
yIdx = axIdx+5;
//velocity1 = velocity0 + acceleration
array[vxIdx] += array[axIdx];
array[vyIdx] += array[ayIdx];
//displacement1 = displacement0 + velocity
array[xIdx] += array[vxIdx];
array[yIdx] += array[vxIdx];
}
この例では、関連付けられた(x、y)を使用したエンティティのレンダリングなどの問題を無視しています...レンダリングには常に非プリミティブ(したがって、参照/ポインター)が必要です。このようなオブジェクトインスタンスが必要な場合は、参照の局所性を保証できなくなり、ヒープ全体を飛び回る可能性があります。したがって、上記のようにプリミティブを多用する処理を行うセクションにコードを分割できる場合、このアプローチは非常に役立ちます。少なくともゲームの場合、AI、動的地形、および物理学は、最もプロセッサを集中的に使用する側面の一部であり、すべて数値であるため、このアプローチは非常に有益です。
数パーセントの改善が違いを生むところまで来ている場合は、50〜100%の改善が得られるCを使用してください!
Javaの使いやすさが使いやすい言語になると思うなら、疑わしい最適化でそれを台無しにしないでください。
幸いなことに、Javaは、実行時にコードを改善するために内部で多くのことを実行しますが、ほぼ確実に、あなたが話しているような最適化は実行しません。
Javaを使用することにした場合は、コードをできるだけ明確に記述してください。マイナーな最適化はまったく考慮しないでください。 (適切なジョブに適切なコレクションを使用したり、ループ内でオブジェクトを割り当てたり解放したりしないなどの主要なものは、まだ価値があります)
これまでのところ、アドバイスはかなり強力です。一般的に、JITを裏切ってみないのが最善です。しかし、あなたが言うように、詳細についてのいくつかの知識は時々役に立ちます。
オブジェクトのメモリレイアウトに関して、SunのJvm(現在はOracle)は、オブジェクトをタイプ別にメモリに配置します(つまり、最初にdoubleとlong、次にintとfloat、次にshortとchar、その後にバイトとブール、最後にオブジェクト参照)。あなたは得ることができます 詳細はこちら 。
ローカル変数は通常、スタック(つまり、参照とプリミティブ型)に保持されます。
Nickが述べているように、Javaのメモリレイアウトを確保する最善の方法は、プリミティブ配列を使用することです。これにより、データがメモリ内で連続していることを確認できます。ただし、配列のサイズには注意してください。GC大きなアレイで問題が発生します。また、メモリ管理を自分で行う必要があるという欠点もあります。
利点として、Flyweightパターンを使用して、高速なパフォーマンスを維持しながら、オブジェクトのような使いやすさを得ることができます。
パフォーマンスをさらに向上させる必要がある場合は、生成されたコードが十分な回数実行され、VMのネイティブコードキャッシュがいっぱいにならない限り、オンザフライで独自のバイトコードを生成すると問題が発生します(これにより、すべての実用的なJITが無効になります)目的)。
私の知る限りでは、いいえ。そのレベルの最適化を実現するには、ほとんどの場合、マシンコードを記述する必要があります。 Assemblyを使用すると、保存場所を制御できなくなるため、一歩先を行くことができます。コンパイラーを使用すると、生成されたコードの詳細を制御することさえできないため、2つのステップがあります。 Javaの場合、コードをその場で解釈するJVMがあるため、3ステップ先にあります。
Javaで、その詳細レベルで物事を制御できる構成要素を知りません。理論的には、プログラムとデータの編成方法によって間接的に影響を与えることができますが、あなたは遠く離れているので、どうすれば確実にそれを実行できるのか、あるいはそれが起こっているかどうかさえわかりません。