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));
_
出力:
ソート済み配列の場合:
_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)
_
未ソートの配列の場合:
_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]
_
出力:
ソート済みリストの場合:
_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)
_
未分類リストの場合
_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)
_
私はいくつかのブログを参照しました this & this これは、同じパフォーマンスの問題(w.r.tストリーム)を示唆しています。
ストリームを使用したプログラミングは、いくつかのシナリオでは素晴らしく、より簡単であるという点に同意しますが、パフォーマンスを失う場合、なぜそれらを使用する必要があるのでしょうか?
パフォーマンスが問題になることはほとんどありません。必要なパフォーマンスを得るには、ストリームの10%をループとして書き換える必要があるのが普通です。
私が見逃しているものはありますか?
ParallelStream()を使用すると、ストリームの使用がはるかに簡単になり、効率的な同時実行コードを記述するのが難しくなるため、おそらくより効率的になります。
ストリームがループと同等に実行されるシナリオはどれですか?定義した関数に多くの時間がかかり、ループのパフォーマンスが無視できる場合にのみですか?
あなたのベンチマークは、コードが開始時にコンパイルされていないという意味で欠陥があります。 JMHのようにテスト全体をループで実行するか、JMHを使用します。
いずれのシナリオでも、分岐予測を利用したストリームを見ることができませんでした
分岐予測は、JVMやストリーム機能ではなくCPU機能です。
Javaは、プログラマが低レベルのパフォーマンスの最適化を検討することを回避する高レベル言語です。
これが実際のアプリケーションの問題であることを証明していない限り、パフォーマンス上の理由から特定のアプローチを選択しないでください。
あなたの測定値は、ストリームに何らかのマイナスの影響を示していますが、その差は観測可能性を下回っています。したがって、それは問題ではありません。また、このテストは「合成」の状況であり、コードはヘビーデューティーの本番環境では完全に異なる動作をする場合があります。さらに、JITによってJava(バイト)コードから作成されたマシンコードは、将来のJava(メンテナンス)リリースで変更され、測定が廃止される可能性があります。
結論:()を最も表現する構文またはアプローチを選択してください(プログラマー)意図。変更する正当な理由がない限り、プログラム全体で同じアプローチまたは構文を使用してください。
すべてが述べられていますが、 [〜#〜] 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最適化には多くの可能性があり、分岐予測も関係していると言えます。ベンチマーク結果を解釈するのはあなた次第です。
ここに0.02 $を追加します。
Branch-Predictionについて読んだところ、Java 8 Streams
分岐予測はCPU機能であり、JVMとは関係ありません。 CPUパイプラインをフルに保ち、何かを行う準備を整えるために必要です。測定または予測分岐予測は非常に困難です(実際にCPUが行う正確なことを知っている場合を除く)。これは、少なくともCPUが現在持っている負荷に依存します(プログラムだけよりもはるかに大きい場合があります)。
ただし、Streamsのパフォーマンスは、従来のループよりも常に劣っています
このステートメントと前のステートメントは無関係です。はい、ストリームは遅くなります単純なあなたのような例、最大30%遅くなりますが、これは問題ありません。 特定の場合他の人が示唆しているようにJMHを介してどれだけ遅いか、または速いかを測定できますが、それはその場合のみ、その負荷のみを証明します。
同時に動作している可能性があります Spring/Hibernate/Servicesなどで、ミリ秒単位で処理を行い、ストリームをナノ秒単位で処理し、パフォーマンスを心配しますか?コードの最速部分の速度に疑問を持っていますか?それはもちろん理論的なことです。
そして、ソートされた配列とソートされていない配列を試してみた最後の点については、悪い結果をもたらします。これは、分岐予測の絶対的な兆候ではありません-予測がどの時点で発生したかわからず、予測が行われた場合はnless実際のCPUパイプラインの内部を見ることができます-しませんでした。
要するに、Javaプログラムは、
はい!
Collection.parallelStream()
および Stream.parallel()
マルチスレッドのメソッドfor
サイクルを書くことができます。ラムダは通常小さく、JITでコンパイルできます=>パフォーマンスを得る可能性がありますfor
ループよりも速くなるシナリオストリームは何ですか?jdk/src/share/vm/runtime/globals.hpp を見てみましょう
develop(intx, HugeMethodLimit, 8000,
"Don't compile methods larger than this if "
"+DontCompileHugeMethods")
十分に長いサイクルがある場合、JITによってコンパイルされず、ゆっくり実行されます。このようなサイクルをストリームに書き換える場合、おそらくコードを断片に分割するmap
、filter
、flatMap
メソッドを使用します。 。確かに、巨大なメソッドを書くことには、JITコンパイル以外の欠点もあります。このシナリオは、たとえば、生成されたコードが大量にある場合に検討できます。
もちろん、ストリームは他のすべてのコードと同様に分岐予測を利用します。ただし、分岐予測は、ストリームをより速くするために明示的に使用されるテクノロジーではありません。
決して。
早すぎる最適化はすべての悪の根源です© Donald Knuth
代わりにアルゴリズムを最適化してください。ストリームは関数型プログラミングのインターフェースであり、ループを高速化するツールではありません。