JAva記述コード_Java 7:非常に高速なJavaコードの作成方法

8579 ワード

JAva作成コード
このブログを初めて書いたとき、Java 7に追加された乱数を生成するためのクラスであるThreadLocalRandomクラスを紹介することを目的としています.
一連のマイクロベンチマーク試験においてThreadLocalRandomの性能を解析し,単一スレッド環境における性能を理解した.
結果は比較的驚きました.コードは非常に似ていますが、ThreadLocalRandomの速度はMath.random()の2倍です.その結果、私の興味を引き起こし、これについてさらに研究することにしました.私はすでに私の分析過程を記録した.これは、分析手順、技術、およびいくつかのJVM診断ツールの例を紹介し、スモールコードセグメントのパフォーマンスの違いを理解します.上記のツールセットとテクノロジーのいくつかの経験を使用すると、特定のHotspotターゲット環境のJavaコードをより速く作成できます.
はい、これで十分です.始めましょう.私のマシンはWindows XPを実行する通常のIntel 386 32ビットデュアルコアです.Math.random()Randomの静的一例を処理し、ThreadLocalRandom -> current() -> nextDouble()ThreadLocalRandomのスレッドローカル例を処理し、この例はRandomのサブクラスである.ThreadLocalは、current()メソッドの呼び出しのたびに変数検索のオーバーヘッドを導入する.私がさっき言ったことを考えると、単一スレッドではMath.random()の2倍の速度ですが、これは本当に驚くべきことですか?こんなに大きな違いがあるとは思わなかった.
同様に、Heinzブログの1つで紹介したマイクロベンチマークテストフレームワークを使用しています.Heinzが開発したフレームワークは,現代のJVM上でJavaプログラムをベンチマークテストする際に直面するいくつかの課題を解決した.これらの課題には、ウォーミングアップ、ゴミ回収、Java時間APIの正確性、テストの正確性の検証などが含まれています.
これは私が実行できるベンチマークテストクラスです.
public class ThreadLocalRandomGenerator implements BenchmarkRunnable {

 private double r;
 
 @Override
 public void run() {
  r = r + ThreadLocalRandom.current().nextDouble();
 }

 public double getR() {
  return r;
 }

 @Override
 public Object getResult() {
  return r;
 }
  
}

public class MathRandomGenerator implements BenchmarkRunnable {

 private double r;

 @Override
 public void run() {
  r = r + Math.random();
 }

 public double getR() {
  return r;
 }

 @Override
 public Object getResult() {
  return r;
 }
}

Heinzのフレームワークを使用してベンチマークテストを実行します.
public class FirstBenchmark {

 private static List benchmarkTargets = Arrays.asList(new MathRandomGenerator(),
   new ThreadLocalRandomGenerator());

 public static void main(String[] args) {
  DecimalFormat df = new DecimalFormat("#.##");
  for (BenchmarkRunnable runnable : benchmarkTargets) {
   Average average = new PerformanceHarness().calculatePerf(new PerformanceChecker(1000, runnable), 5);
   System.out.println("Benchmark target: " + runnable.getClass().getSimpleName());
   System.out.println("Mean execution count: " + df.format(average.mean()));
   System.out.println("Standard deviation: " + df.format(average.stddev()));
   System.out.println("To avoid dead code coptimization: " + runnable.getResult());
  }
 }
}

注意:JVMがコードをデッドコードとして識別しないように、フィールド変数を返し、すぐにベンチマークテストの結果を印刷します.それが私の実行可能クラスがRunnableBenchmarkというインタフェースを実現した理由です.私はもう3回ベンチマークテストを実行しました.最初の実行は、デフォルトモードでインラインおよびJIT最適化が有効になっています.
Benchmark target: MathRandomGenerator
Mean execution count: 14773594,4
Standard deviation: 180484,9
To avoid dead code coptimization: 6.4005410634212025E7
Benchmark target: ThreadLocalRandomGenerator
Mean execution count: 29861911,6
Standard deviation: 723934,46
To avoid dead code coptimization: 1.0155096190946539E8

その後、JIT最適化は再度行われない(VMオプション-Xint):
Benchmark target: MathRandomGenerator
Mean execution count: 963226,2
Standard deviation: 5009,28
To avoid dead code coptimization: 3296912.509302683
Benchmark target: ThreadLocalRandomGenerator
Mean execution count: 1093147,4
Standard deviation: 491,15
To avoid dead code coptimization: 3811259.7334526842

最後のテストはJIT最適化を使用したが、-XX:MaxInlineSize=0を使用してインラインを無効にした.
Benchmark target: MathRandomGenerator
Mean execution count: 13789245
Standard deviation: 200390,59
To avoid dead code coptimization: 4.802723374491231E7
Benchmark target: ThreadLocalRandomGenerator
Mean execution count: 24009159,8
Standard deviation: 149222,7
To avoid dead code coptimization: 8.378231170741305E7

結果を詳細に説明すると、完全なJVM JIT最適化により、ThreadLocalRanomの速度はMath.random()の2倍である.JIT最適化をオフにすると,両者の性能は同じ(悪い)ことが分かった.メソッドのインライン化により、パフォーマンスが30%異なるようです.他の相違は、他の最適化技術に起因する可能性があります.
JITコンパイラがThreadLocalRandomをより効果的に調整できる理由の1つは、ThreadLocalRandom.next()の改良された実装である.
public class Random implements java.io.Serializable {
...
    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }
...
}

public class ThreadLocalRandom extends Random {
...
    protected int next(int bits) {
        rnd = (rnd * multiplier + addend) & mask;
        return (int) (rnd >>> (48-bits));
    }
...
}

第1のセグメントはRandom.next()を示し、Math.random()のベンチマーク試験で多く使用された.ThreadLocalRandom.next()と比較して、この方法は、両方の方法が同じことをしているにもかかわらず、より多くの命令を必要とする.Randomクラスでは、seed変数はグローバル共有状態をすべてのスレッドに格納し、next()メソッドが呼び出されるたびに変更します.したがって、AtomicLong呼び出しのnextDouble()値に安全にアクセスし、変更する必要がある.一方、seedは-良い-スレッドローカル:-)ThreadLocalRandomメソッドであり、スレッドが安全である必要はなく、通常のnext()変数をシード値として使用することができる.
メソッドインラインおよびlongについて
方法インラインは非常に有効なJIT最適化である.頻繁に実行されるホットパスでは、ホットスポットコンパイラは、呼び出されたメソッド(サブメソッド)のコードを呼び出し元メソッド(親メソッド)に関連付けることを決定します.インラインには重要なメリットがあります.メソッド呼び出しの動的周波数を大幅に低減し、これらのメソッド呼び出しを実行するのに要する時間を節約します.しかし、より重要なのは、最適化プログラムで使用するために、インラインがより大きなコードブロックを生成することです.これにより、従来のコンパイラの最適化の効率が大幅に向上し、Javaプログラミング言語の性能向上の主な障害を克服することができます.」
Java 7からは、診断JVMオプション監視メソッドを使用してインラインできます.「ThreadLocalRandom」を使用してコードを実行すると、JITコンパイラのインライン動作が表示されます.以下は-XX:+UnlockDiagnosticVMOptions -XX:+PrintInliningベンチマークテスト出力の関連部分です.
@ 13   java.util.Random::nextDouble (24 bytes)
  @ 3   java.util.Random::next (47 bytes)   callee is too large
  @ 13   java.util.Random::next (47 bytes)   callee is too large

JITコンパイラは、Math.random()で呼び出されたRandom.next()を接続できません.これはRandom.nextDouble()のインライン出力です.
@ 8   java.util.Random::nextDouble (24 bytes)
  @ 3   java.util.concurrent.ThreadLocalRandom::next (31 bytes)
  @ 13   java.util.concurrent.ThreadLocalRandom::next (31 bytes)
ThreaLocalRandom.next()の方法は短い(31バイト)ため、インラインすることができる.両方のベンチマークテストでnext()メソッドが強く呼び出されるため、ログは、メソッドのインラインがnext()の実行速度を著しく向上させる原因の1つである可能性があることを示している.
より多くの情報を検証し発見するためには、アセンブリコードを深く検討する必要があります.Java 7 JDKを使用すると、アセンブリコードをコンソールに印刷できます.ThreadLocalRandom VMオプションを有効にする方法については、ここを参照してください.このオプションでは、JIT最適化コードが印刷されます.これは、JVMが実際に実行しているコードが表示されることを意味します.関連するアセンブリコードを次のリンクにコピーしました.
ここのThreadLocalRandomGenerator.run()のアセンブリコード.MathRandomGenerator.run()のアセンブリコードはここにあります.Math.random()ここで呼び出されたRandom.next()のアセンブリコード.
アセンブリコードは機械固有の低級コードであり,bytecodeよりもずっと複雑である.私のベンチマークテストで方法のインラインが性能に及ぼす影響を検証してみましょう.また、JITコンパイラが-XX:+PrintAssemblyThreadLocalRandom()にどのように対応しているか、他に明らかな違いがありますか.Math.randomでは、ThreadLocalRandomGenerator.run()またはRandom.nextDouble()などのサブルーチンは呼び出されません.ThreatLocalRandom.next()には仮想的な(したがって高価な)メソッドのみが呼び出されます(ThreadLocal.get()プログラムセットの35行を参照)..他のすべてのコードはThreadLocalRandomGenerator.run()に接続されています.この場合、ThreadLocalRandomGenerator.run()には2つの仮想メソッドがMathRandomGenerator.run()ブロックB 4線204ページおよび以降のアセンブリコードRandom.next()に呼び出されていることが確認されています.すなわち、メソッドの接続がパフォーマンスの違いをもたらす重要な根本的な原因であることが確認されています.また、同期の煩わしさから、MathRandomGenerator.run()に必要なアセンブリ命令はより多く(しかも少し高価!)、これは実行速度の面でも逆効果です.Random.next()命令のオーバーヘッドについて
では、なぜ(仮想)メソッド呼び出しは高価であり、メソッドインラインはこのように有効ですか?invokevirtual命令のポインタは、クラスインスタンス内の特定のメソッドのオフセット量ではありません.コンパイラは、クラスインスタンスの内部レイアウトを知りません.逆に、インスタンスメソッドに対するシンボル参照を生成します.これらのシンボル参照は、実行時定数プールに格納されます.これらの実行時定数プール項目は、実行時に解析され、実際のメソッドの場所を決定します.を選択します.このダイナミック(ランタイム)バインドは、パフォーマンスに大きな影響を及ぼす可能性がある検証、準備、解決が必要です.(詳細は、JVM仕様の呼び出し方法とリンクを参照してください).
これまではそうでした.免責声明:もちろん、パフォーマンスの課題を解決するために理解しなければならないトピックのリストは尽きません.マイクロベンチマークテスト、JIT最適化、メソッドインライン、javaバイトコード、assemby言語などのほか、理解すべきことはたくさんあります.同様に、仮想メソッド呼び出しや高価なスレッド同期命令のほか、パフォーマンスの違いをもたらすものもたくさんあります.根本的な原因です.しかし、私が紹介したテーマはこのような深い研究の良い始まりだと思います.批判と楽しいコメントを期待しています.
参考資料:JCGパートナーのNiklasからの「Java 7:非常に高速なJavaコードの作成方法」.
翻訳:https://www.javacodegeeks.com/2012/01/java-7-how-to-write-really-fast-java.html
JAva作成コード