JavaはC / C ++と比較してパフォーマンスを「微調整」するのがはるかに難しいですか?[閉まっている]


11

JVMの「魔法」は、プログラマーがJavaのマイクロ最適化に与える影響を妨げますか?私は最近C ++で読んだことがありますが、データメンバーの順序が最適化を提供できる場合があります(マイクロ秒環境で許可されます)。

まともなアルゴリズムがより大きな速度向上を提供することを感謝しますが、正しいアルゴリズムを取得すると、JVM制御のためにJavaを微調整するのが難しくなりますか?

そうでない場合は、Javaで使用できるトリックの例を(単純なコンパイラフラグのほかに)挙げてください。


14
すべてのJava最適化の背後にある基本原則は次のとおりです。おそらく、JVMはすでにあなたよりも上手く行っているでしょう。最適化には、主に賢明なプログラミング手法に従うこと、およびループ内で文字列を連結するなどの通常のことを回避することが含まれます。
ロバートハーヴェイ

3
すべての言語でのマイクロ最適化の原則は、コンパイラーが既にあなたよりも上手く行っているということです。すべての言語でのマイクロ最適化のもう1つの原則は、ハードウェアを追加するほうがプログラマーがマイクロ最適化するよりも安いということです。プログラマーはスケーリングの問題(次善のアルゴリズム)を行う傾向がありますが、マイクロ最適化は時間の無駄です。ハードウェアを追加できない組み込みシステムでは、マイクロ最適化が意味をなす場合がありますが、Javaを使用するAndroidとその実装が貧弱であるため、ほとんどのハードウェアが既に十分なハードウェアを備えていることがわかります。
ジャン・ヒューデック

1
「Javaのパフォーマンスのトリック」のため、価値が勉強である:効果的なJavaのアンジェリカランガーリンク- Javaのパフォーマンスおよびパフォーマンス関連の記事ブライアン・ゲッツによるにおけるJavaの理論と実践スレッディングは軽く シリーズがリストされ、ここで
ブヨ

2
ヒントやコツについては十分に注意してください- JVM、オペレーティングシステムやハードウェアに移動するには-あなたのしている最高のパフォーマンス・チューニングの方法論を学習し、ための拡張機能を適用するオフあなたの特定の環境:-)
マルタインVerburg

場合によっては、VMは実行時に最適化を行うことができますが、これはコンパイル時に行うのは実用的ではありません。マネージメモリを使用するとパフォーマンスが向上しますが、多くの場合、メモリフットプリントが大きくなります。未使用のメモリは、できるだけ早くではなく、都合の良いときに解放されます。
ブライアン

回答:


5

確かに、マイクロ最適化レベルでは、JVMは、特にCやC ++と比較してほとんど制御できないことを行います。

一方、CおよびC ++を使用したさまざまなコンパイラーの動作は、(コンパイラーのリビジョンを問わず)漠然と移植可能な方法でマイクロ最適化を実行する能力に特に大きな悪影響を与えます。

それは、どの種類のプロジェクトを調整しているのか、どの環境をターゲットにしているのかなどに依存します。とにかく、アルゴリズム/データ構造/プログラム設計の最適化から数桁優れた結果が得られているので、結局のところ、それは本当に重要ではありません。


あなたのアプリがコアでスケールしない見つけたとき、それは多くの問題ではできる
ジェームズ

@james-細心の注意が必要ですか?
テラスティン

1
開始についてはこちらをご覧ください:mechanical-sympathy.blogspot.co.uk/2011/07/false-sharing.html-
ジェームズ

1
@James、コア全体のスケーリングは、実装言語(Pythonを除く!)とはほとんど関係がなく、アプリケーションアーキテクチャとも関係があります。
ジェームスアンダーソン

29

マイクロ最適化はほとんど時間の価値がなく、ほとんどすべての簡単な最適化はコンパイラとランタイムによって自動的に行われます。

ただし、C ++とJavaが根本的に異なる最適化の1つの重要な領域があり、それはバルクメモリアクセスです。C ++には手動のメモリ管理があります。つまり、アプリケーションのデータレイアウトとアクセスパターンを最適化して、キャッシュを最大限に活用できます。これは非常に難しく、実行しているハードウェアにある程度固有のものです(したがって、異なるハードウェアではパフォーマンスの向上が見られない場合があります)。もちろん、あなたはあらゆる種類の恐ろしいバグの可能性でそれを支払います。

Javaのようなガベージコレクションされた言語では、この種の最適化はコードで実行できません。ランタイムによって実行できるものもあります(自動または構成により、以下を参照)。また、不可能なものもあります(メモリ管理のバグから保護されるために支払う代償)。

そうでない場合は、Javaで使用できるトリックの例を(単純なコンパイラフラグのほかに)挙げてください。

Javaコンパイラは最適化をほとんど行わないため、コンパイラフラグはJavaでは無関係です。ランタイムは行います。

実際、Javaランタイムには、特にガベージコレクターに関して調整可能な多数のパラメーターあります。これらのオプションには「単純な」ものはありません。デフォルトはほとんどのアプリケーションに適しています。パフォーマンスを向上させるには、オプションの動作とアプリケーションの動作を正確に理解する必要があります。


1
+1:基本的に、回答で書いていたもの、おそらくより良い定式化。
クライム

1
+1:非常に良い点で、非常に簡潔な方法で説明されています:「これは非常に難しいです...しかし、正しく行われた場合、それは絶対に息をのむようなパフォーマンスにつながる可能性があります。 」
ジョルジオ

1
@MartinBa:メモリ管理を最適化するために支払う金額はそれ以上です。メモリ管理を最適化しようとしない場合、C ++メモリ管理はそれほど難しくありません(STLを介して完全に回避するか、RAIIを使用して比較的簡単にします)。もちろん、C ++でRAIIを実装すると、Javaで何もしないよりも多くのコード行が必要になります(つまり、Javaがそれを処理するためです)。
ブライアン

3
@Martin Ba:基本的にはい。宙ぶらりんのポインター、バッファーオーバーフロー、初期化されていないポインター、ポインター演算のエラー、手動メモリ管理なしでは存在しないすべてのもの。また、メモリアクセスを最適化するには、多くの手動メモリ管理が必要です。
マイケルボルグワード

1
Javaでできることはいくつかあります。1つはオブジェクトプーリングで、オブジェクトのメモリローカリティを最大化します(メモリローカリティを保証できるC ++とは異なります)。
RokL

5

[...](マイクロ秒環境で許可)[...]

数百万から数十億をループする場合、マイクロ秒が加算されます。C ++からの個人的なvtune / micro-optimizationセッション(アルゴリズムの改善なし):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

「マルチスレッド」、「SIMD」(コンパイラーに勝る手書き)、および4価パッチ最適化以外はすべて、マイクロレベルのメモリ最適化でした。また、32秒の初期時間から始まる元のコードは既にかなり最適化されており(理論的に最適なアルゴリズムの複雑さ)、これは最近のセッションです。この最近のセッションが処理するのに5分以上かかったずっと前の元のバージョン。

メモリ効率の最適化は、シングルスレッドコンテキストでは数倍から数桁まで、そしてマルチスレッドコンテキストではさらに多くの場合に役立ちます(効率的なメモリ担当者の利点は、多くの場合、複数のスレッドで増加します)。

マイクロ最適化の重要性について

マイクロ最適化は時間の無駄であるというこの考えに、私は少し動揺します。私はそれが良い一般的なアドバイスであることに同意しますが、誰もが測定ではなく勘や迷信に基づいて間違ってそれを行うわけではありません。正しく行われた場合、必ずしも小さな影響を与えるとは限りません。Intel独自のEmbree(レイトレーシングカーネル)を使用して、作成した単純なスカラーBVHのみをテストし(ビートが指数関数的に難しいレイパケットではなく)、そのデータ構造のパフォーマンスをビートしようとすると、何十年もの間コードのプロファイリングとチューニングに慣れていたベテランでさえ、謙虚な経験。そしてそれはすべて、最適化が適用されているためです。彼らのソリューションは、レイトレーシングで働いている産業の専門家を見たときに、毎秒1億本以上の光線を処理できます。

アルゴリズムに焦点を絞ったBVHの簡単な実装を採用し、最適化コンパイラ(IntelのICCを含む)に対して毎秒1億を超えるプライマリレイの交差を取得する方法はありません。簡単なものは、多くの場合、1秒あたり100万の光線さえも取得しません。毎秒数百万本の光線を得るためにも、プロ品質のソリューションが必要です。毎秒1億本以上の光線を取得するには、Intelレベルのマイクロ最適化が必要です。

アルゴリズム

数分から数秒、または数時間から数分のレベルでパフォーマンスが重要でない限り、マイクロ最適化は重要ではないと思います。バブルソートのような恐ろしいアルゴリズムを例として大規模な入力で使用し、それをマージソートの基本的な実装と比較した場合、前者は処理に数か月かかり、後者はおそらく12分かかります。二次対線形の複雑さ。

数か月と数分の違いにより、パフォーマンスが重要な分野で働いていない人も含め、ほとんどの人が結果を得るまでに数か月待たなければならない場合、実行時間は許容できないと考えられるでしょう。

一方、マイクロ最適化されていない単純なマージソートをクイックソートと比較すると(マージソートよりもアルゴリズム的に優れているわけではなく、参照の局所性に対してマイクロレベルの改善しか提供されません)、マイクロ最適化されたクイックソートは12分ではなく15秒。ユーザーを12分間待機させることは、完全に受け入れられる場合があります(コーヒーブレイクのような時間)。

この違いは、たとえば12分から15秒の間、ほとんどの人にとっておそらく無視できると思います。そのため、分と月ではなく分と秒の違いにしかすぎないことが多いため、マイクロ最適化は役に立たないと見なされることがよくあります。私がそれが役に立たないと思うもう一つの理由は、それが重要でないエリアにしばしば適用されるということです:ループでなく、クリティカルでさえない1%の違いを生じる小さなエリア(非常によくちょうどノイズかもしれません)。しかし、これらのタイプの時間差を気にし、それを適切に測定して実行したい人にとっては、少なくともメモリ階層の基本概念(特にページフォールトとキャッシュミスに関連する上位レベル)に注意を払う価値があると思います。

Javaは、優れたマイクロ最適化の余地を十分に残しています

ええ、ごめんなさい-その種の暴言は別として:

JVMの「魔法」は、プログラマーがJavaのマイクロ最適化に与える影響を妨げますか?

少しだけですが、あなたが正しくやれば人々が考えるほどではありません。たとえば、手書きのSIMD、マルチスレッド、メモリ最適化(アクセスパターン、場合によっては画像処理アルゴリズムに応じた表現)を使用したネイティブコードで画像処理を行う場合、32ビットRGBAピクセル(8ビットカラーチャンネル)であり、1秒あたり数十億もの場合もあります。

Pixelオブジェクトを作成したと言うと、Javaのどこにでも近づくことは不可能です(これだけでは、ピクセルのサイズが4ビットから64ビットで16に膨れ上がります)。

しかし、Pixelオブジェクトを避け、バイトの配列を使用し、Imageオブジェクトをモデル化すれば、全体をもっと近づけることができるかもしれません。プレーンな古いデータの配列を使用し始めた場合、Javaはまだかなり有能です。私はJavaで前に物事のこれらの種類を試してみた、非常に感銘を受けました提供、通常よりも4倍大きいであることどこでもあなたが少しちっちゃいの束を作成しないことをオブジェクト(例:使用intの代わりInteger)などバルク・インタフェースをモデル化開始しますImageインターフェイスではなく、Pixelインターフェイス。オブジェクトではなくプレーンな古いデータ(floatたとえば、notの巨大な配列)をループしている場合、JavaはC ++のパフォーマンスに匹敵すると言えますFloat

おそらく、メモリサイズよりもさらに重要なのは、配列がint連続表現を保証することです。の配列はありIntegerません。連続性は、複数の要素(例:16 ints)がすべて単一のキャッシュラインに収まり、効率的なメモリアクセスパターンでエビクションする前に一緒にアクセスされる可能性があることを意味するため、参照の局所性にとってしばしば不可欠です。一方、単一のIntegerメモリはメモリ内のどこかに取り残され、周囲のメモリは無関係であり、メモリのその領域を16行の整数ではなく、エビクションの前に単一の整数を使用するためにのみキャッシュラインにロードします。たとえ私たちが見事に幸運で周りを取り囲んだとしてもIntegersメモリ内で互いに隣接していたため、Integer4倍の大きさの結果として、立ち退き前にアクセスできるキャッシュラインに4のみを収めることができます。これはベストケースのシナリオです。

また、同じメモリアーキテクチャ/階層の下で統合されているため、そこには多くのマイクロ最適化があります。メモリアクセスパターンは、使用する言語に関係なく、ループタイル/ブロッキングなどの概念は一般にCまたはC ++ではるかに頻繁に適用される可能性がありますが、Javaでも同様に役立ちます。

最近、C ++で読んだことがありますが、データメンバーの順序によって最適化が可能になる場合があります[...]

一般的に、Javaではデータメンバーの順序は重要ではありませんが、それはほとんど良いことです。CおよびC ++では、ABIの理由でデータメンバの順序を維持することが重要になる場合が多いため、コンパイラはそれを混乱させません。そこで作業する人間の開発者は、パディングでメモリを浪費しないように、データメンバーを降順(最大から最小)に並べるなどの注意を払う必要があります。Javaを使用すると、明らかにJITは、パディングを最小限に抑えながら適切なアライメントを確保するために、その場でメンバーを並べ替えることができるので、そうであれば、平均的なCおよびC ++プログラマーがしばしばうまくいかず、メモリをそのように浪費することを自動化します(これは単にメモリを浪費するだけでなく、AoS構造間のストライドを不必要に増やし、キャッシュミスを増やすことで速度を浪費することがよくあります)。それ' パディングを最小限に抑えるためにフィールドを再配置する非常にロボット的なことなので、理想的には人間はそれに対処しません。オブジェクトが64バイトよりも大きく、アクセスパターン(最適なパディングではない)に基づいてフィールドを配置する場合に、最適な配置を人間が知る必要がある方法でフィールド配置が重要になる場合があります-その場合より人間的な努力かもしれません(クリティカルパスを理解する必要があります。その一部は、ユーザーがソフトウェアで何をするかを知らずにコンパイラが予測できない情報です)。

そうでない場合は、Javaで使用できるトリックの例を(単純なコンパイラフラグのほかに)挙げてください。

JavaとC ++の間のメンタリティを最適化するという点での私にとっての最大の違いは、パフォーマンスが重要なシナリオでC ++を使用すると、Javaよりも少し(十数)多くオブジェクトを使用できることです。たとえば、C ++は、オーバーヘッドをまったく発生させずに整数をクラスにラップできます(あらゆる場所でベンチマークが行われます)。Javaは、オブジェクトごとにメタデータポインタースタイル+アライメントパディングのオーバーヘッドを必要とするため、これBooleanよりも大きくなりますboolean(ただし、リフレクションの均一な利点と、finalすべての単一UDT としてマークされていない機能をオーバーライドする機能を提供します)。

C ++では、空間的な局所性が失われることが多い(または少なくとも制御が失われる)ため、不均一なフィールド間でメモリレイアウトの連続性を制御するのが少し簡単です(例:構造体/クラスを介してfloatとintを1つの配列にインターリーブする) JavaでGCを介してオブジェクトを割り当てるとき。

...しかし、多くの場合、最高のパフォーマンスのソリューションはしばしばそれらをとにかく分割し、プレーンな古いデータの連続した配列でSoAアクセスパターンを使用します。そのため、ピークパフォーマンスが必要な領域では、JavaとC ++の間のメモリレイアウトを最適化する戦略が同じであることが多く、ホット/ (あなただけのバイトまたはそのような何かの生の配列を使用していない限り)寒冷フィールド分割、SOAは担当者など非均質AoSoA担当者は、Javaで不可能のようなものに見えるが、これらはまれなケースのためにあるの両方順次およびランダムアクセスパターンは高速であると同時に、ホットフィールドのフィールドタイプが混在している必要があります。私にとって、これら2つの間の最適化戦略(一般的なレベルで)の違いの大部分は、ピークパフォーマンスに到達する場合には意味がありません。

以下のような小さなオブジェクトを持つ限り行うことができるそこにいるではない-あなたは、単に「良い」パフォーマンスのために達している場合の違いは、よりかなり変化Integerint特にそれがジェネリック医薬品と対話の方法で、もう少しPITAのことができます。それはのために働くことにJavaでちょうどビルドに少し難しく中央最適化の対象として1つの汎用データ構造だintfloatそれらの大きく、高価なのUDTを回避しながらなど、が、多くの場合、最もパフォーマンスが重要な領域は、手圧延独自のデータ構造が必要になりますとにかく非常に特定の目的のために調整されているため、最高のパフォーマンスではなく良好なパフォーマンスを目指しているコードにとっては迷惑です。

オブジェクトのオーバーヘッド

Javaオブジェクトのオーバーヘッド(メタデータと空間的局所性の喪失、および最初のGCサイクル後の一時的局所性の一時的喪失)は、数百万単位のデータ構造に数百万単位で格納されている非常に小さいもの(int対などInteger)でしばしば大きくなることに注意してくださいほぼ連続しており、非常にタイトなループでアクセスされます。このテーマには多くの感度があるように思えるので、画像のような大きなオブジェクトのオブジェクトのオーバーヘッドを心配したくないことを明確にする必要があります。

誰かがこの部分に疑問を感じるならints、100万ランダムIntegersと100 万ランダムを合計し、これを繰り返し行うことの間のベンチマークを作成することをお勧めします(Integers最初のGCサイクル後にメモリ内でシャッフルされます)。

Ultimate Trick:最適化の余地を残すインターフェイス設計

:あなたは小さなオブジェクト(例:オーバーハンドル高負荷というところを扱っている場合、私はそれを見るように究極のJavaトリックだからPixel、4 -ベクトル、4x4の行列、Particleおそらく、でもAccountそれだけでいくつかの小さなを持っている場合フィールド)は、これらの小さな事柄にオブジェクトを使用することを避け、単純な古いデータの配列(おそらく連鎖されている)を使用することです。その後のようなコレクションインタフェースになるオブジェクトImageParticleSystemAccountsでも、その基本的なオブジェクトのオーバーヘッドなしにするので、これはまた、CおよびC ++での究極のデザインのトリックの一つである例えば、個々のものは、インデックスによってアクセスすることができるなどの行列やベクトルの集合と、ばらばらのメモリ、単一粒子のレベルでインターフェースをモデリングすると、最も効率的なソリューションが妨げられます。


1
バルクでのパフォーマンスの低下は、実際には重要な領域でピークパフォーマンスを圧倒する可能性があることを考えると、パフォーマンスが良好であることの利点を完全に無視できるとは思いません。また、構造体の配列を配列の構造体に変換するトリックは、元の構造体の1つを構成するすべての(またはほぼすべての)値が同時にアクセスされると、多少壊れます。ところで:私はあなたが;-)、時には良い答えをやや古い記事をたくさん発掘し、独自の良い答えを追加している参照
デュプリケータ

1
@Deduplicator Hopeぶつかりすぎて人に迷惑をかけないように!これはちょっとした不満がありました-多分私はそれを少し改善する必要があります。SoA対AoSは、私にとって難しいものです(シーケンシャルアクセスとランダムアクセス)。私の場合、シーケンシャルアクセスとランダムアクセスが混在していることが多いため、どちらを使用するかを事前に知ることはほとんどありません。私がよく学んだ貴重な教訓は、データ表現で遊ぶのに十分なスペースを残すインターフェースを設計することです-可能な場合は大きな変換アルゴリズムを持つちょっとかさばるインターフェース(時々、ランダムにアクセスされる小さなビットでは不可能)

1
まあ、私は物事が本当に遅いので気づいただけです。そして、私はそれぞれと時間をかけました。
デデュプリケーター

なぜuser204677去ったのか不思議です。そのような素晴らしい答え。
オリゴフレン

3

一方ではマイクロ最適化と、他方ではアルゴリズムの適切な選択との中間の領域があります。

これは、定数係数の高速化の領域であり、桁違いの結果をもたらす可能性があります。
その方法は、最初の30%、残りの20%、残りの50%など、実行時間の一部を切り捨てることで、何回か繰り返して、残りがほとんどなくなるまで続けます。

これは、小さなデモスタイルのプログラムでは見られません。ご覧の場所は、多くのクラスデータ構造を持つ大きな深刻なプログラムで、通常、コールスタックは多くの層の深さです。高速化の機会を見つける良い方法は、プログラムの状態のランダムな時間サンプル調べることです。

通常、高速化は次のようなもので構成されます。

  • new古いオブジェクトをプールして再利用することでへの呼び出しを最小限に抑え、

  • 実際に必要なのではなく、一般性のためにそこにあるようなものを認識し、

  • 同じbig-O動作を持つが、実際に使用されるアクセスパターンを利用する異なるコレクションクラスを使用してデータ構造を修正する。

  • 関数を再呼び出しする代わりに関数呼び出しによって取得されたデータを保存します(プログラマーは、短い名前の関数がより速く実行されると想定するのが自然で面白い傾向です)。

  • 通知イベントとの完全な一貫性を維持しようとするのではなく、冗長データ構造間の一定量の不一致を許容します。

  • などなど

しかし、もちろん、これらのことは、サンプルを取ることによって最初に問題であることが示されない限り、行われるべきではありません。


2

Java(私が知っている限り)は、メモリ内の変数の場所を制御できないため、変数の誤った共有や整列などを避けるのが難しくなります(いくつかの未使用のメンバーでクラスを埋めることができます)。私があなたが利用できるとは思わないもう一つのことは、などの命令ですがmmpause、これらはCPU固有のものです。

C / C ++の柔軟性を提供するだけでなく、C / C ++の危険を伴うUnsafeクラスが存在します。

JVMがコード用に生成するアセンブリコードを確認すると役立つ場合があります

この種の詳細を調べるJavaアプリについて読むには、LMAXによってリリースされたディスラプターコードを参照てください。


2

この質問は、言語の実装に依存するため、答えるのは非常に困難です。

一般的に、最近ではこのような「マイクロ最適化」の余地はほとんどありません。主な理由は、コンパイラがコンパイル中にそのような最適化を利用することです。たとえば、セマンティクスが同一の状況では、プリインクリメント演算子とポストインクリメント演算子の間にパフォーマンスの違いはありません。別の例としては、たとえば、次のようなループfor(int i=0; i<vec.size(); i++)を呼び出します。size()各反復中のメンバー関数では、ループの前にベクトルのサイズを取得し、その単一変数と比較して、反復ごとの関数呼び出しを回避する方が良いでしょう。ただし、コンパイラがこの愚かなケースを検出し、結果をキャッシュする場合があります。ただし、これは、関数に副作用がなく、ループ中にベクトルサイズが一定のままであることをコンパイラが確認できる場合にのみ可能です。これは、かなり些細な場合にのみ適用されます。


2番目のケースについては、コンパイラが近い将来に最適化できるとは思いません。vec.size()を安全に最適化できるかどうかを判断するには、ループ内でvector / lostが変化しない場合のサイズを証明する必要があります。
ライライアン

@LieRyan結果が手動で「キャッシュ」され、size()が呼び出された場合、コンパイラがまったく同じバイナリファイルを生成する、複数の(単純な)ケースを見てきました。いくつかのコードを書きましたが、動作はプログラムの動作に大きく依存していることがわかりました。コンパイラーは、ループ中にベクターのサイズが変更される可能性がないことを保証できる場合があり、その後、あなたが述べたように停止の問題と非常によく似て、それを保証できない場合があります。今のところ、私は自分の主張を検証することができません(C ++の分解は苦痛です)ので、答えを編集しました
-zxcdw

2
@Lie Ryan:一般的なケースでは決定できないものの多くは、特定の一般的なケースでは完全に決定可能であり、本当に必要なのはここだけです。
マイケルボルグワード

@LieRyan constこのベクターのメソッドのみを呼び出す場合、多くの最適化コンパイラーがそれを見つけ出すと確信しています。
K.ステフ

C#では、Javaで読んだと思いますが、サイズをキャッシュしないと、コンパイラはチェックを削除して配列の境界外にいるかどうかを確認でき、キャッシュサイズを実行する場合はチェックを行う必要があることを知っています、通常、キャッシュによる節約よりも費用がかかります。オプティマイザーを凌martすることは、めったに良い計画ではありません。
ケイトグレゴリー

1

Javaで使用できるトリックの例を(単純なコンパイラフラグに加えて)挙げることができます。

アルゴリズムの改善以外に、メモリ階層とプロセッサがそれをどのように利用するかを必ず検討してください。問題の言語がメモリをデータ型とオブジェクトにどのように割り当てるかを理解すると、メモリアクセスの待ち時間を短縮することに大きな利点があります。

1000x1000 intの配列にアクセスするJavaの例

以下のサンプルコードを検討してください。同じメモリ領域(intの1000x1000配列)にアクセスしますが、順序は異なります。私のmac mini(Core i7、2.7 GHz)では、出力は次のようになり、行でアレイを走査するとパフォーマンスが2倍以上になることを示しています(各100ラウンドの平均)。

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

これは、連続した列(つまりint値)がメモリ内で隣接して配置されるように配列が格納されるのに対し、連続する行は格納されないためです。プロセッサが実際にデータを使用するには、キャッシュに転送する必要があります。メモリの転送は、キャッシュラインと呼ばれるバイトブロック単位で行われます。キャッシュラインをメモリから直接ロードすると、レイテンシが発生し、プログラムのパフォーマンスが低下します。

Core i7(サンディブリッジ)の場合、キャッシュラインは64バイトを保持するため、各メモリアクセスは64バイトを取得します。最初のテストは予測可能な順序でメモリにアクセスするため、プロセッサはデータがプログラムによって実際に消費される前にデータをプリフェッチします。全体的に、これによりメモリアクセスのレイテンシが減少し、パフォーマンスが向上します。

サンプルのコード:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }

1

JVMは干渉する可能性があり、多くの場合、JITコンパイラーはバージョン間で大幅に変更される可能性があります。

ディスラプターの作成者のトピックに関する非常に有益なブログを読むことをお勧めします。

マイクロ最適化が必要な場合、なぜJavaを使用するのが面倒なのかを常に尋ねる必要があります。JNAまたはJNIを使​​用してネイティブライブラリに渡すなど、関数を高速化するための多くの代替方法があります。

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