SKB KonturのforEach(ps :: println)に関するタスク

JBreakカンファレンスは、スポンサーのタスクを意図的に読んでいませんでした。 まあ、もちろん、 Excelsior地獄を除いて:これらの人は本当にみんなに熱を設定します。 そして、彼らは私にSKB Konturのシートを持ってきて、見て、笑って言った。 私は笑いました。最初のタスクは本当に素朴に形成され、未決定でしたので、スタンドに行って会社の従業員にこれを納得させたくさえありませんでした。 私はそれをほとんど忘れていましたが、ここでHabréでこのタスクの作者の分析がありましたmodCount



modCount



についてmodCount



書きmodCount



。 私は無駄に笑っていたことが判明しましたか?







思い出させてください、タスクは次のように聞こえました:







作業の速度でメソッドを並べ替え、適切なセルにメソッドの数を入力します。 メソッドの結果がわずかに異なる場合は、1つのセルに入力してください。 各タスクについて、一部のメソッドが他のメソッドよりも高速である理由の説明を書きます。 コードはHotSpot 64ビットVM(JRE 1.8.0_161)で実行されることを前提としています。

 void forEach(List<Integer> values, PrintStream ps) { // 1 values.forEach(ps::println); } void forEach(List<Integer> values, PrintStream ps) { // 2 values.stream().forEach(ps::println); } void forEach(List<Integer> values, PrintStream ps) { // 3 values.parallelStream().forEach(ps::println); }
      
      





この問題の著者によると、正解は「に依存する」:空の星の位置に応じて1または2です。 同時に、決定のためのフォームはそのようなオプションを含むことを意味しませんでした、そしてこの場所の説明でさえ十分ではありませんでした。 一般的に、明確な正解なしにタスクを与えることは間違っていると思います。 一意に2つの正解が存在する問題に反対ではありません:1と2(たとえば、両方とも常に、そして、証明可能に同等に高速です)。 しかし、ここに答えがあります。状況に応じて、これまたはそれのいずれかです。 これはい作業であり、想像力に欠けます。 著者には1つの正解があったようですが、その後(おそらく会議参加者と連絡を取った後)自分たちが間違っていることに気づきました。







ただし、最大の問題は、状況によっては3番目のオプションが最速であることです。







思い出させてください: parallelStream



オプションに対する著者の主張は、コンシューマ( PrintStream::println



)が同期されることです。つまり、データ消費プロセスは線形化されます。つまり、2つの要素の消費の間で発生前関係が確立されます。 したがって、並列化のメリットは得られず、スレッドの作成と同期のオーバーヘッドのみが得られます。 それでも、ストリームがデータコンシューマだけでなくソースからも構成されていることを忘れないでください。 また、ソースは線形化されない場合があります。 ソースとコンシューマーを選択して、ソースの速度を大幅に低下させると、並列処理によりプロセッサー数に近い任意の増加が得られます。







タスクの用語では、ソースはList



インターフェイスの実装の一種です。 タスクはそれ以上の制限を導入しないため、契約を尊重する実装を自由に引用できます。 意図的に過度の遅延を発生させることなく、論理的に見える「合理的に合理的な実装」に制限することもできます。







まず消費者をスピードアップしましょう。 もちろん、 PrintStream



は最終クラスではありませんprintln



メソッドをオーバーライドし、一般的な同期を削除することで簡単に拡張できます。 ただし、私はこの不正行為を検討します。既存の動作を再定義することは、このような問題の正しい解決策と見なされるべきではありません。 それでも、 PrintStream



OutputStream



単なるラッパーであり、この抽象クラスの実装をスリップする権利があることを覚えておいてください。たとえば、何もしません:







 private static PrintStream getStream() { return new PrintStream(new OutputStream() { public void write(int b) {} public void write(byte[] b, int off, int len) {} }); }
      
      





もちろん、これは必要ではありませんが、効果を高めるためだけです。 また、標準のコンソール出力のソースを作成することもできますが、これははるかに遅くなります。







今、実際には、ソース。 アイテムをゆっくり発行するリストは非常に可能性が高く、人生では非常に一般的です。 これは、たとえば、適切な関数を選択することにより、GuavaライブラリのLists.transform使用して作成できます。 手動で作成するのも簡単です。get get()



size()



2つのメソッドを定義して、標準のAbstractListを展開するだけです。 リストはランダムアクセスリストになるため、RandomAccessマーカーインターフェイスでもマークします。







リストで何をしますか? たとえば、インデックスi



各要素には、UTF-16エンコーディングのインデックスの文字列表現からのSHA-512ハッシュ合計の下位バイトが含まれるようにします。 どうして? たぶん、このような擬似ランダムで安定したものが必要です:







 static List<Integer> getList(int size) { class MyList extends AbstractList<Integer> implements RandomAccess { @Override public Integer get(int index) { // ,     Java 9,  ! Objects.checkIndex(index, size); try { MessageDigest md = MessageDigest.getInstance("SHA-512"); md.update(String.valueOf(index).getBytes(StandardCharsets.UTF_16)); return (int) md.digest()[0]; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Override public int size() { return size; } } return new MyList(); }
      
      





ベンチマークの完全なソースコードを表示する場合は、ここをクリックしてください。
 @BenchmarkMode(Mode.AverageTime) @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Fork(5) @State(Scope.Benchmark) public class ListBenchmark { @Param({"100", "10000", "1000000"}) private int size; @Benchmark public void testForEach() { forEach1(getList(size), getStream()); } @Benchmark public void testStreamForEach() { forEach2(getList(size), getStream()); } @Benchmark public void testParallelStreamForEach() { forEach3(getList(size), getStream()); } void forEach1(List<Integer> values, PrintStream ps) { // 1 values.forEach(ps::println); } void forEach2(List<Integer> values, PrintStream ps) { // 2 values.stream().forEach(ps::println); } void forEach3(List<Integer> values, PrintStream ps) { // 3 values.parallelStream().forEach(ps::println); } static PrintStream getStream() { return new PrintStream(new OutputStream() { public void write(int b) {} public void write(byte[] b, int off, int len) {} }); } static List<Integer> getList(int size) { class MyList extends AbstractList<Integer> implements RandomAccess { @Override public Integer get(int index) { // ,     Java 9,  ! Objects.checkIndex(0, size); try { MessageDigest md = MessageDigest.getInstance("SHA-512"); md.update(String.valueOf(index).getBytes(StandardCharsets.UTF_16)); return (int) md.digest()[0]; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Override public int size() { return size; } } return new MyList(); } }
      
      





ここで、4コアマシンでベンチマークを実行し、次の結果を確認します。







 # JMH version: 1.20 # VM version: JDK 9.0.4, VM 9.0.4+11 # VM invoker: C:\Program Files\Java\jdk-9.0.4\bin\java.exe ... Benchmark (size) Mode Cnt Score Error Units testForEach 100 avgt 50 317,811 ± 48,847 us/op testForEach 10000 avgt 50 11944,452 ± 308,630 us/op testForEach 1000000 avgt 50 1650511,656 ± 489838,860 us/op testParallelStreamForEach 100 avgt 50 89,836 ± 2,507 us/op testParallelStreamForEach 10000 avgt 50 9629,607 ± 935,511 us/op testParallelStreamForEach 1000000 avgt 50 890954,996 ± 64176,572 us/op testStreamForEach 100 avgt 50 171,991 ± 48,877 us/op testStreamForEach 10000 avgt 50 22063,496 ± 5965,278 us/op testStreamForEach 1000000 avgt 50 1646935,273 ± 485037,889 us/op
      
      





ForEachとStreamForEachの結果は少し奇妙に見えます。 たとえば、100個の要素のリストの場合はストリームが優先され、10,000個のリストの場合は逆になります。 ただし、望ましい結果は明らかです。どこでも並列ストリームが高速に動作します。







ところで、そのような変動はどこにありますか? SKB Konturの開発者の間違いを繰り返さないと、簡単に発生する可能性があります。同じフォークでベンチマークを実行することはできません。特に、結果がJITコンパイラの作業によって著しく影響を受けると疑われる場合は。 JITコンパイラーは完全に非決定的です。コンパイル結果はアセンブルされたプロファイルに依存しますが、プロファイルは非同期に収集されるため、コンパイル時に異なる番号が存在し、コードが異なります。 安定した状態に達すると、ホットコードは再コンパイルされないため、反復回数を増やすことは意味がありません。 たとえば、ForEach / size = 100の出力を参照してください。







おわりに
 # Run progress: 0,00% complete, ETA 00:05:37 # Fork: 1 of 5 # Warmup Iteration 1: 585,124 us/op # Warmup Iteration 2: 390,473 us/op # Warmup Iteration 3: 388,228 us/op # Warmup Iteration 4: 387,198 us/op # Warmup Iteration 5: 384,001 us/op Iteration 1: 386,163 us/op Iteration 2: 376,344 us/op Iteration 3: 374,520 us/op Iteration 4: 364,211 us/op Iteration 5: 364,389 us/op Iteration 6: 363,269 us/op Iteration 7: 364,266 us/op Iteration 8: 364,123 us/op Iteration 9: 363,860 us/op Iteration 10: 364,035 us/op # Run progress: 2,22% complete, ETA 00:05:48 # Fork: 2 of 5 # Warmup Iteration 1: 426,376 us/op # Warmup Iteration 2: 386,556 us/op # Warmup Iteration 3: 382,808 us/op # Warmup Iteration 4: 378,838 us/op # Warmup Iteration 5: 377,987 us/op Iteration 1: 377,438 us/op Iteration 2: 374,149 us/op Iteration 3: 370,854 us/op Iteration 4: 361,177 us/op Iteration 5: 362,603 us/op Iteration 6: 361,055 us/op Iteration 7: 361,445 us/op Iteration 8: 361,564 us/op Iteration 9: 364,283 us/op Iteration 10: 359,619 us/op # Run progress: 4,44% complete, ETA 00:05:39 # Fork: 3 of 5 # Warmup Iteration 1: 582,933 us/op # Warmup Iteration 2: 144,197 us/op # Warmup Iteration 3: 140,762 us/op # Warmup Iteration 4: 135,908 us/op # Warmup Iteration 5: 132,585 us/op Iteration 1: 132,768 us/op Iteration 2: 132,491 us/op Iteration 3: 129,447 us/op Iteration 4: 119,316 us/op Iteration 5: 119,427 us/op Iteration 6: 119,018 us/op Iteration 7: 119,305 us/op Iteration 8: 119,361 us/op Iteration 9: 119,130 us/op Iteration 10: 119,105 us/op # Run progress: 6,67% complete, ETA 00:05:31 # Fork: 4 of 5 # Warmup Iteration 1: 569,460 us/op # Warmup Iteration 2: 389,247 us/op # Warmup Iteration 3: 385,768 us/op # Warmup Iteration 4: 381,309 us/op # Warmup Iteration 5: 379,797 us/op Iteration 1: 381,618 us/op Iteration 2: 372,816 us/op Iteration 3: 371,384 us/op Iteration 4: 361,205 us/op Iteration 5: 361,428 us/op Iteration 6: 361,174 us/op Iteration 7: 360,579 us/op Iteration 8: 360,488 us/op Iteration 9: 359,859 us/op Iteration 10: 361,365 us/op # Run progress: 8,89% complete, ETA 00:05:23 # Fork: 5 of 5 # Warmup Iteration 1: 544,560 us/op # Warmup Iteration 2: 390,766 us/op # Warmup Iteration 3: 388,537 us/op # Warmup Iteration 4: 383,953 us/op # Warmup Iteration 5: 382,270 us/op Iteration 1: 384,325 us/op Iteration 2: 377,098 us/op Iteration 3: 371,531 us/op Iteration 4: 362,499 us/op Iteration 5: 362,045 us/op Iteration 6: 363,345 us/op Iteration 7: 361,930 us/op Iteration 8: 362,357 us/op Iteration 9: 362,452 us/op Iteration 10: 362,302 us/op
      
      





結果として、2つの異なるモードがあります:1つは約360マイクロ秒、もう1つは約120マイクロ秒、同時に、フォークのフレームワーク内では、結果はフォーク間よりもはるかに安定しています。 タスクの無意味さを感じ始めていますか? 同じコードを再起動した場合、どのようなパフォーマンスの違いを確認できますか。実行が3倍遅くなります。 JITはしばしばそのようなトリックを投げかけます。 あなたが幸運であり、JITが10分の1を採用して成功した決定を下した実験に基づいて結論を出すのは不快です。 原則として、3つ未満のフォークを作成することはありません。速度の疑わしい違いに気付いた場合は、現象をより詳細に調査するためにそれらの数を増やします。







それにもかかわらず、結論を研究して、私は自信を持って、最も遅い並列フォークでさえ、最も速い順次フォークより悪くはないと言うことができます。 著者によると、これは「明らかな間違った答え」ですが、3番目の選択肢が勝つことができます。







気配りのある読者は反対します。let-let、それはどんな種類のJava 9ですか? 用語はJava 1.8.0_161でした。 はい、発言は正しいです。私はここで実際に使用する機能を使用します。これは、9つだけにあります: List.spliteratorはRandomAccessリスト用に最適化する必要があります 。 Java 8では、リストのスプリッターのデフォルトの実装はより愚かであり、並列化ははるかに悪いです(そして、Javaの古いバージョンを使用するものは何もありません!)。したがって、Java 8のそのようなコードでは、ほとんど成長しません。 しかし、これは私たちの推論を根本的に間違ったものにするものではなく、もう少しコードを書くだけです。 本格的な優れたスプリッターを作成するにはかなり時間がかかりましたが、場合によっては(ここを含む)、フリーズして代わりにstream()



およびparallelStream()



メソッドを再度実装できます。







 class MyList extends AbstractList<Integer> implements RandomAccess { @Override public Integer get(int index) { //   checkIndex   Java 8 :-( if(index < 0 || index >= size) throw new IndexOutOfBoundsException(); ...      ... } @Override public int size() { return size; } @Override public Stream<Integer> stream() { return IntStream.range(0, size).mapToObj(this::get); } @Override public Stream<Integer> parallelStream() { return stream().parallel(); } }
      
      





これは不正行為とはみなされません。 同様に実装さCollections.nCopies



。たとえば、 Collections.nCopies



によって返されるリストのストリーム。 並列ストリームのソースとして使用する予定のリストを作成する場合は、もちろん、それが正常に並列することを確認する必要があります。







結果は非常に期待されています。







 # JMH version: 1.20 # VM version: JDK 1.8.0_161, VM 25.161-b12 # VM invoker: C:\Program Files\Java\jre1.8.0_161\bin\java.exe Benchmark (size) Mode Cnt Score Error Units testForEach 100 avgt 50 134,974 ± 2,900 us/op testForEach 10000 avgt 50 13075,674 ± 332,211 us/op testForEach 1000000 avgt 50 1315215,358 ± 9702,119 us/op testParallelStreamForEach 100 avgt 50 113,029 ± 1,934 us/op testParallelStreamForEach 10000 avgt 50 9711,281 ± 113,490 us/op testParallelStreamForEach 1000000 avgt 50 1002479,362 ± 11129,949 us/op testStreamForEach 100 avgt 50 134,033 ± 2,806 us/op testStreamForEach 10000 avgt 50 13247,368 ± 273,218 us/op testStreamForEach 1000000 avgt 50 1320319,189 ± 16277,564 us/op
      
      





そして、並列ストリームが勝ちます。 ちなみに、8つの結果は明らかに安定しています。 おそらく、JITコンパイラーの9つで何かが壊れました。 興味深いことに、Java 10ではJava 8よりも安定していると同時に高速です。







結論は次のように描くことができます。 パフォーマンスは、その領域で信頼できるタスクを作成するには薄すぎます。 最後の手段として、たとえば、 以前の競争アパンギンが行ったように、「より速い」タスクではなく「効果を説明する」タスクを定式化することができます(2番目の質問を参照)。 一般に、Stream APIを使用すると、すべてが簡単ではありません。自由な変数と微妙な効果が多すぎて、間違いなく何かを語ることができません。 正確さとセマンティクスのためのパズルをより良く発明し、すべてがより厳格になります。







SKB Konturの残りのタスクを解析することを楽しみにしています!








All Articles