web-dev-qa-db-ja.com

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

JVMの「魔法」は、プログラマーがJavaのマイクロ最適化に及ぼす影響を妨げますか?私は最近C++で読みました。時々、データメンバーの順序が最適化(マイクロ秒環境で許可)を提供できることがあり、Javaからのパフォーマンスの絞り込みに関してプログラマーの手が縛られていると思いますか?

まともなアルゴリズムを使用すると速度が向上しますが、正しいアルゴリズムを使用すると、Java JVM制御のために調整が難しくなりますか?

そうでない場合は、Java(単純なコンパイラフラグのほか)で使用できるトリックの例を人々に示すことができますか?.

11
user997112

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

一方、CおよびC++でのさまざまなコンパイラーの動作は、(コンパイラーのリビジョンを超えて)なんらかの移植性のない方法でマイクロ最適化を実行する機能に、はるかに大きな悪影響を及ぼします。

調整するプロジェクトの種類、対象とする環境などによって異なります。とにかく、アルゴリズム/データ構造/プログラム設計の最適化から数桁優れた結果が得られるので、結局は問題ではありません。

5
Telastyn

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

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

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

そうでない場合は、Java(単純なコンパイラフラグのほか)で使用できるトリックの例を人々に示すことができますか?.

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

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

29

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

これは、定数係数の高速化の領域であり、桁違いの速度が得られます。
そのようにする方法は、実行時間の全体の端数を最初から30%、次に残りの20%、次に50%のように、ほとんど何もなくなるまで繰り返します。左。

これは小さなデモ形式のプログラムでは見られません。あなたがそれを見るところは、多くのクラスデータ構造を持つ大きな深刻なプログラムであり、コールスタックは通常多くの層の深さです。スピードアップの機会を見つける良い方法は、プログラムの状態の examing random-time samples によるものです。

一般的に、スピードアップは次のようなもので構成されています。

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

  • 実際に必要になるのではなく、一般性のためにそこで行われていることを認識し、

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

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

  • 通知データと完全に一致させようとするのではなく、冗長データ構造間の一定量の不一致を許容します。

  • などなど.

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

3
Mike Dunlavey

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

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

2
zxcdw

Java(私が知っている限り)では、メモリ内の変数の場所を制御できないため、変数の偽共有や整列などを回避するのが困難になります(クラスを未使用のメンバーで埋め込むことができます)。あなたが利用できないと思うもう1つのことは、mmpauseなどの命令ですが、これらはCPU固有であるため、必要な場合はJava使用する言語であること。

Unsafe クラスが存在し、C/C++の柔軟性を提供しますが、C/C++の危険も伴います。

それはあなたのコードのためにJVMが生成する アセンブリコードを見るのを助けるかもしれません

Javaアプリについて読むには、 LMAXによってリリースされたDisruptorコードを参照してください

2
James

Java(単純なコンパイラフラグのほか)で使用できるトリックの例を人々に示すことができます。

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

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

以下のサンプルコードを見てください。同じメモリ領域(intの1000x1000配列)にアクセスしますが、順序は異なります。私のMac mini(Core i7、2.7 GHz)では、出力は次のようになります。double以上の行で配列をトラバースすると、パフォーマンスが向上します(それぞれ平均100ラウンド)。

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

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

Core i7(Sandy Bridge)の場合、キャッシュラインは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
miraculixx

JVMは干渉する可能性があり、しばしば干渉し、JITコンパイラはバージョン間で大幅に変化する可能性がありますJavaでは、ハイパースレッド対応または最新のIntelなどの言語制限により、一部のマイクロ最適化は不可能ですプロセッサのSIMDコレクション。

Disruptor の著者からのトピックに関する非常に有益なブログを読むことをお勧めします:

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

1
Steve-o