Java 8は初めてです。APIの詳細はまだわかりませんが、新しい非公式のベンチマークを作成して、新しいStreams APIと古き良きコレクションのパフォーマンスを比較しました。
このテストは、Integer
のリストをフィルタリングし、偶数ごとに平方根を計算し、それをList
of Double
に保存することで構成されます。
コードは次のとおりです。
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 1000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add(Math.sqrt(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}.
デュアルコアマシンの結果は次のとおりです。
Collections: Elapsed time: 94338247 ns (0,094338 seconds)
Streams: Elapsed time: 201112924 ns (0,201113 seconds)
Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds)
この特定のテストでは、ストリームはコレクションの約2倍の速度であり、並列処理は役に立ちません(または、間違った方法で使用していますか?)。
質問:
結果を更新しました。
@pveentjerのアドバイスに従って、JVMのウォームアップ(1k回の反復)後に1k回テストを実行しました。
Collections: Average time: 206884437,000000 ns (0,206884 seconds)
Streams: Average time: 98366725,000000 ns (0,098367 seconds)
Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds)
この場合、ストリームのパフォーマンスが向上します。実行時にフィルター機能が1回または2回しか呼び出されないアプリでは、何が観察されるのでしょうか。
イテレータを使用してリストの中央から重い削除を行う以外は、LinkedList
の使用を停止してください。
手作業でベンチマークコードの記述を停止し、 JMH を使用します。
適切なベンチマーク:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
public static final int N = 10000;
static List<Integer> sourceList = new ArrayList<>();
static {
for (int i = 0; i < N; i++) {
sourceList.add(i);
}
}
@Benchmark
public List<Double> Vanilla() {
List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
for (Integer i : sourceList) {
if (i % 2 == 0){
result.add(Math.sqrt(i));
}
}
return result;
}
@Benchmark
public List<Double> stream() {
return sourceList.stream()
.filter(i -> i % 2 == 0)
.map(Math::sqrt)
.collect(Collectors.toCollection(
() -> new ArrayList<>(sourceList.size() / 2 + 1)));
}
}
結果:
Benchmark Mode Samples Mean Mean error Units
StreamVsVanilla.stream avgt 10 17.588 0.230 ns/op
StreamVsVanilla.Vanilla avgt 10 10.796 0.063 ns/op
予想どおり、ストリームの実装はかなり遅くなります。 JITはすべてのラムダをインライン化できますが、Vanillaバージョンほど完全に簡潔なコードを生成しません。
一般的に、Java 8ストリームは魔法ではありません。既に十分に実装されたものを高速化できませんでした(おそらく、単純な反復またはJava 5のfor-eachステートメントがIterable.forEach()
およびCollection.removeIf()
呼び出しに置き換えられます)。ストリームは、コーディングの利便性と安全性に関するものです。利便性-速度のトレードオフがここで機能しています。
1)ベンチマークを使用すると、1秒未満の時間が表示されます。つまり、結果に副作用が強く影響する可能性があります。だから、私はあなたのタスクを10倍に増やしました
int max = 10_000_000;
ベンチマークを実行しました。私の結果:
Collections: Elapsed time: 8592999350 ns (8.592999 seconds)
Streams: Elapsed time: 2068208058 ns (2.068208 seconds)
Parallel streams: Elapsed time: 7186967071 ns (7.186967 seconds)
編集なし(int max = 1_000_000
)の結果は
Collections: Elapsed time: 113373057 ns (0.113373 seconds)
Streams: Elapsed time: 135570440 ns (0.135570 seconds)
Parallel streams: Elapsed time: 104091980 ns (0.104092 seconds)
それはあなたの結果のようです:ストリームはコレクションよりも遅いです。 結論:ストリームの初期化/値の送信に多くの時間が費やされました。
2)タスクストリームの増加後は高速になりました(それで問題ありません)が、パラレルストリームは遅すぎます。どうしましたか?注:コマンドにはcollect(Collectors.toList())
があります。単一のコレクションに収集すると、基本的に同時実行の場合にパフォーマンスのボトルネックとオーバーヘッドが発生します。を置き換えることにより、オーバーヘッドの相対コストを推定することが可能です
collecting to collection -> counting the element count
ストリームの場合は、collect(Collectors.counting())
で実行できます。私は結果を得ました:
Collections: Elapsed time: 41856183 ns (0.041856 seconds)
Streams: Elapsed time: 546590322 ns (0.546590 seconds)
Parallel streams: Elapsed time: 1540051478 ns (1.540051 seconds)
それは大きな仕事です! (int max = 10000000
)結論:コレクションへのアイテムの収集には大部分の時間がかかりました。最も遅い部分はリストへの追加です。ところで、シンプルなArrayList
はCollectors.toList()
に使用されます。
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 10000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add( doSomeCalculate(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}
static double doSomeCalculate(int input) {
for(int i=0; i<100000; i++){
Math.sqrt(i+input);
}
return Math.sqrt(input);
}
コードを少し変更し、8コアのMacブックプロで実行すると、妥当な結果が得られました。
コレクション:経過時間:1522036826 ns(1.522037秒)
ストリーム:経過時間:4315833719 ns(4.315834秒)
並列ストリーム:経過時間:261152901 ns(0.261153秒)
あなたがやろうとしていることのために、とにかく通常のJava APIを使用しません。大量のボクシング/アンボクシングが行われているため、パフォーマンスに大きなオーバーヘッドがあります。
個人的には、設計された多くのAPIは、大量のオブジェクトリターを作成するため、くだらないと思います。
Double/intのプリミティブ配列を使用し、シングルスレッドで実行して、パフォーマンスを確認してください。
PS:ベンチマークを行うためにJMHをご覧になることをお勧めします。 JVMのウォームアップなどの典型的な落とし穴のいくつかを処理します。