web-dev-qa-db-ja.com

最高のパフォーマンスを得るには、従来のループよりもいつストリームを優先すべきですか?ストリームは分岐予測を利用しますか?

Branch-Prediction について読み、Java 8 Streams。

ただし、Streamsのパフォーマンスは、従来のループよりも常に劣っています。

_int totalSize = 32768;
int filterValue = 1280;
int[] array = new int[totalSize];
Random rnd = new Random(0);
int loopCount = 10000;

for (int i = 0; i < totalSize; i++) {
    // array[i] = rnd.nextInt() % 2560; // Unsorted Data
    array[i] = i; // Sorted Data
}

long start = System.nanoTime();
long sum = 0;
for (int j = 0; j < loopCount; j++) {
    for (int c = 0; c < totalSize; ++c) {
        sum += array[c] >= filterValue ? array[c] : 0;
    }
}
long total = System.nanoTime() - start;
System.out.printf("Conditional Operator Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    for (int c = 0; c < totalSize; ++c) {
        if (array[c] >= filterValue) {
            sum += array[c];
        }
    }
}
total = System.nanoTime() - start;
System.out.printf("Branch Statement Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    sum += Arrays.stream(array).filter(value -> value >= filterValue).sum();
}
total = System.nanoTime() - start;
System.out.printf("Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    sum += Arrays.stream(array).parallel().filter(value -> value >= filterValue).sum();
}
total = System.nanoTime() - start;
System.out.printf("Parallel Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));
_

出力:

  1. ソート済み配列の場合:

    _Conditional Operator Time : 294062652 ns, (0.294063 sec) 
    Branch Statement Time : 272992442 ns, (0.272992 sec) 
    Streams Time : 806579913 ns, (0.806580 sec) 
    Parallel Streams Time : 2316150852 ns, (2.316151 sec) 
    _
  2. 未ソートの配列の場合:

    _Conditional Operator Time : 367304250 ns, (0.367304 sec) 
    Branch Statement Time : 906073542 ns, (0.906074 sec) 
    Streams Time : 1268648265 ns, (1.268648 sec) 
    Parallel Streams Time : 2420482313 ns, (2.420482 sec) 
    _

Listを使用して同じコードを試しました:
list.stream()の代わりにArrays.stream(array)の代わりに
list.get(c)の代わりに_array[c]_

出力:

  1. ソート済みリストの場合:

    _Conditional Operator Time : 860514446 ns, (0.860514 sec) 
    Branch Statement Time : 663458668 ns, (0.663459 sec) 
    Streams Time : 2085657481 ns, (2.085657 sec) 
    Parallel Streams Time : 5026680680 ns, (5.026681 sec) 
    _
  2. 未分類リストの場合

    _Conditional Operator Time : 704120976 ns, (0.704121 sec) 
    Branch Statement Time : 1327838248 ns, (1.327838 sec) 
    Streams Time : 1857880764 ns, (1.857881 sec) 
    Parallel Streams Time : 2504468688 ns, (2.504469 sec) 
    _

私はいくつかのブログを参照しました thisthis これは、同じパフォーマンスの問題(w.r.tストリーム)を示唆しています。

  1. ストリームを使用したプログラミングは、一部のシナリオでは便利で簡単ですが、パフォーマンスが低下した場合、なぜそれらを使用する必要があるのか​​という点には同意します。私が見逃しているものはありますか?
  2. ストリームがループと同等に実行されるシナリオはどれですか?定義された関数に多くの時間がかかり、ループのパフォーマンスが無視できる場合のみですか?
  3. シナリオのいずれでも、branch-predictionを利用するストリームを見ることができませんでした通常のストリームと比較してパフォーマンスへの影響が2倍以上)
42
Bandi Kishore

ストリームを使用したプログラミングは、いくつかのシナリオでは素晴らしく、より簡単であるという点に同意しますが、パフォーマンスを失う場合、なぜそれらを使用する必要があるのでしょうか?

パフォーマンスが問題になることはほとんどありません。必要なパフォーマンスを得るには、ストリームの10%をループとして書き換える必要があるのが普通です。

私が見逃しているものはありますか?

ParallelStream()を使用すると、ストリームの使用がはるかに簡単になり、効率的な同時実行コードを記述するのが難しくなるため、おそらくより効率的になります。

ストリームがループと同等に実行されるシナリオはどれですか?定義した関数に多くの時間がかかり、ループのパフォーマンスが無視できる場合にのみですか?

あなたのベンチマークは、コードが開始時にコンパイルされていないという意味で欠陥があります。 JMHのようにテスト全体をループで実行するか、JMHを使用します。

いずれのシナリオでも、分岐予測を利用したスト​​リームを見ることができませんでした

分岐予測は、JVMやストリーム機能ではなくCPU機能です。

42
Peter Lawrey

Javaは、プログラマが低レベルのパフォーマンスの最適化を検討することを回避する高レベル言語です。

これが実際のアプリケーションの問題であることを証明していない限り、パフォーマンス上の理由から特定のアプローチを選択しないでください。

あなたの測定値は、ストリームに何らかのマイナスの影響を示していますが、その差は観測可能性を下回っています。したがって、それは問題ではありません。また、このテストは「合成」の状況であり、コードはヘビーデューティーの本番環境では完全に異なる動作をする場合があります。さらに、JITによってJava(バイト)コードから作成されたマシンコードは、将来のJava(メンテナンス)リリースで変更され、測定が廃止される可能性があります。

結論:)を最も表現する構文またはアプローチを選択してください(プログラマー)意図。変更する正当な理由がない限り、プログラム全体で同じアプローチまたは構文を使用してください。

27
Timothy Truckle

すべてが述べられていますが、 [〜#〜] jmh [〜#〜] を使用して、コードがどのように見えるかを示したいと思います。

@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@Measurement(iterations = 10, timeUnit = TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Threads(1)
@Warmup(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {

  private final int totalSize = 32_768;
  private final int filterValue = 1_280;
  private final int loopCount = 10_000;
  // private Random rnd;

  private int[] array;

  @Setup
  public void setup() {
    array = IntStream.range(0, totalSize).toArray();

    // rnd = new Random(0);
    // array = rnd.ints(totalSize).map(i -> i % 2560).toArray();
  }

  @Benchmark
  public long conditionalOperatorTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      for (int c = 0; c < totalSize; ++c) {
        sum += array[c] >= filterValue ? array[c] : 0;
      }
    }
    return sum;
  }

  @Benchmark
  public long branchStatementTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      for (int c = 0; c < totalSize; ++c) {
        if (array[c] >= filterValue) {
          sum += array[c];
        }
      }
    }
    return sum;
  }

  @Benchmark
  public long streamsTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      sum += IntStream.of(array).filter(value -> value >= filterValue).sum();
    }
    return sum;
  }

  @Benchmark
  public long parallelStreamsTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      sum += IntStream.of(array).parallel().filter(value -> value >= filterValue).sum();
    }
    return sum;
  }
}

ソートされた配列の結果:

Benchmark                            Mode  Cnt           Score           Error  Units
MyBenchmark.branchStatementTime      avgt   30   119833793,881 ±   1345228,723  ns/op
MyBenchmark.conditionalOperatorTime  avgt   30   118146194,368 ±   1748693,962  ns/op
MyBenchmark.parallelStreamsTime      avgt   30   499436897,422 ±   7344346,333  ns/op
MyBenchmark.streamsTime              avgt   30  1126768177,407 ± 198712604,716  ns/op

ソートされていないデータの結果:

Benchmark                            Mode  Cnt           Score           Error  Units
MyBenchmark.branchStatementTime      avgt   30   534932594,083 ±   3622551,550  ns/op
MyBenchmark.conditionalOperatorTime  avgt   30   530641033,317 ±   8849037,036  ns/op
MyBenchmark.parallelStreamsTime      avgt   30   489184423,406 ±   5716369,132  ns/op
MyBenchmark.streamsTime              avgt   30  1232020250,900 ± 185772971,366  ns/op

JVM最適化には多くの可能性があり、分岐予測も関係していると言えます。ベンチマーク結果を解釈するのはあなた次第です。

16
Flown

ここに0.02 $を追加します。

Branch-Predictionについて読んだところ、Java 8 Streams

分岐予測はCPU機能であり、JVMとは関係ありません。 CPUパイプラインをフルに保ち、何かを行う準備を整えるために必要です。測定または予測分岐予測は非常に困難です(実際にCPUが行う正確なことを知っている場合を除く)。これは、少なくともCPUが現在持っている負荷に依存します(プログラムだけよりもはるかに大きい場合があります)。

ただし、Streamsのパフォーマンスは、従来のループよりも常に劣っています

このステートメントと前のステートメントは無関係です。はい、ストリームは遅くなります単純なあなたのような例、最大30%遅くなりますが、これは問題ありません。 特定の場合他の人が示唆しているようにJMHを介してどれだけ遅いか、または速いかを測定できますが、それはその場合のみ、その負荷のみを証明します。

同時に動作している可能性があります Spring/Hibernate/Servicesなどで、ミリ秒単位で処理を行い、ストリームをナノ秒単位で処理し、パフォーマンスを心配しますか?コードの最速部分の速度に疑問を持っていますか?それはもちろん理論的なことです。

そして、ソートされた配列とソートされていない配列を試してみた最後の点については、悪い結果をもたらします。これは、分岐予測の絶対的な兆候ではありません-予測がどの時点で発生したかわからず、予測が行われた場合はnless実際のCPUパイプラインの内部を見ることができます-しませんでした。

10
Eugene

どうすればJavaプログラムを高速で実行できますか?

要するに、Javaプログラムは、

  1. マルチスレッド
  2. JIT

ストリームはJavaプログラムの高速化に関連していますか?

はい!

  1. Collection.parallelStream() および Stream.parallel() マルチスレッドのメソッド
  2. JITがスキップするのに十分な長さのforサイクルを書くことができます。ラムダは通常小さく、JITでコンパイルできます=>パフォーマンスを得る可能性があります

forループよりも速くなるシナリオストリームは何ですか?

jdk/src/share/vm/runtime/globals.hpp を見てみましょう

develop(intx, HugeMethodLimit,  8000,
        "Don't compile methods larger than this if "
        "+DontCompileHugeMethods")

十分に長いサイクルがある場合、JITによってコンパイルされず、ゆっくり実行されます。このようなサイクルをストリームに書き換える場合、おそらくコードを断片に分割するmapfilterflatMapメソッドを使用します。 。確かに、巨大なメソッドを書くことには、JITコンパイル以外の欠点もあります。このシナリオは、たとえば、生成されたコードが大量にある場合に検討できます。

分岐予測についてはどうですか?

もちろん、ストリームは他のすべてのコードと同様に分岐予測を利用します。ただし、分岐予測は、ストリームをより速くするために明示的に使用されるテクノロジーではありません。

だから、いつ最高のパフォーマンスを達成するためにループをストリームに書き換えますか?

決して。

早すぎる最適化はすべての悪の根源です© Donald Knuth

代わりにアルゴリズムを最適化してください。ストリームは関数型プログラミングのインターフェースであり、ループを高速化するツールではありません。

4
Sergey Fedorov