階乗計算または強度ストリームAPI

先日、 5nwという記事が登場し、階乗をすばやく計算する2つの方法がありました 。これは、「分割統治」の原則に従って乗算された数値をツリーにグループ化することで階乗の計算を高速化するアイデアを提供します。 これを見て、すぐにここで並列Javaスレッドがすべての栄光を見せていることに気付きました。結局、このようにスプリッターの助けを借りてタスクをサブタスクに分割します。 迅速な実装も美しいことがわかります。



public static BigInteger streamedParallel(int n) { if(n < 2) return BigInteger.valueOf(1); return IntStream.rangeClosed(2, n).parallel().mapToObj(BigInteger::valueOf).reduce(BigInteger::multiply).get(); }
      
      







残念ながら、記事5nwにはパフォーマンスの測定に関する詳細はありません。 いくつのテストが実行されましたか? ウォームアップはありましたか? 時間測定誤差は推定されましたか? JITコンパイラーは、結果が使用されなかったため、計算の一部を刈りましたか? また、使用した場合(たとえば、結果の数値がファイルに表示された場合)、使用の事実は時間のカウントから除外されますか? この点で、 JMHライブラリと多数のプレゼンテーションや記事を通じて、Javaコミュニティにある種のベンチマーク文化を浸透させたAlexei Shipilevに感謝します。



このようなベンチマークコードがあります。

 import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.annotations.*; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import java.math.BigInteger; @Warmup(iterations=5) @Measurement(iterations=10) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Benchmark) @Fork(2) public class Factorial { @Param({"10", "100", "1000", "10000", "50000"}) private int n; public static BigInteger naive(int n) { BigInteger r = BigInteger.valueOf(1); for (int i = 2; i <= n; ++i) r = r.multiply(BigInteger.valueOf(i)); return r; } public static BigInteger streamed(int n) { if(n < 2) return BigInteger.valueOf(1); return IntStream.rangeClosed(2, n).mapToObj(BigInteger::valueOf).reduce(BigInteger::multiply).get(); } public static BigInteger streamedParallel(int n) { if(n < 2) return BigInteger.valueOf(1); return IntStream.rangeClosed(2, n).parallel().mapToObj(BigInteger::valueOf).reduce(BigInteger::multiply).get(); } @Benchmark public void testNaive(Blackhole bh) { bh.consume(naive(n)); } @Benchmark public void testStreamed(Blackhole bh) { bh.consume(streamed(n)); } @Benchmark public void testStreamedParallel(Blackhole bh) { bh.consume(streamedParallel(n)); } }
      
      







ナイーブ、通常のスレッド、並列スレッドの3つの実装を比較しました。 結果のダイナミクスを表示するために、n-10、100、1000、10000、および50000のさまざまな値のアルゴリズムをチェックするのが妥当です。 ウォームアップの5回の反復と測定を伴う10回の反復が実行されます。これはすべて(Javaマシンの再起動により)2回繰り返されます。つまり、テストごとに20回の測定が行われます。 ブラックホールに注意してください。JITコンパイラーが計算の結果を削除せず、使用されないと判断するために必要です。



Core i7-4702MQプロセッサ(8つのハードウェアスレッド)を搭載したラップトップでテストしました。 結果は次のとおりです。



 Benchmark (n) Mode Cnt Score Error Units Factorial.testNaive 10 avgt 20 0.298 ± 0.005 us/op Factorial.testNaive 100 avgt 20 5.113 ± 0.195 us/op Factorial.testNaive 1000 avgt 20 278.601 ± 9.586 us/op Factorial.testNaive 10000 avgt 20 32232.618 ± 889.512 us/op Factorial.testNaive 50000 avgt 20 972369.158 ± 29287.950 us/op Factorial.testStreamed 10 avgt 20 0.339 ± 0.021 us/op Factorial.testStreamed 100 avgt 20 5.432 ± 0.246 us/op Factorial.testStreamed 1000 avgt 20 268.366 ± 4.859 us/op Factorial.testStreamed 10000 avgt 20 30429.526 ± 435.611 us/op Factorial.testStreamed 50000 avgt 20 896719.207 ± 7995.599 us/op Factorial.testStreamedParallel 10 avgt 20 6.470 ± 0.515 us/op Factorial.testStreamedParallel 100 avgt 20 11.280 ± 1.094 us/op Factorial.testStreamedParallel 1000 avgt 20 74.174 ± 2.647 us/op Factorial.testStreamedParallel 10000 avgt 20 2826.643 ± 52.831 us/op Factorial.testStreamedParallel 50000 avgt 20 49196.070 ± 464.634 us/op
      
      





単純なテストは全体として、アルゴリズムの2次の複雑さの理論的仮定を確認します。 時間をn²で除算します。



 n = 10: 0.002980 n = 100: 0.000511 n = 1000: 0.000279 n = 10000: 0.000322 n = 50000: 0.000389
      
      





大きな値での増加は、おそらくメモリ消費の増加とガベージコレクターのアクティブ化に関連しています。



小さいnの比類のないストリームは期待どおりに動作し、わずかに長い時間(n = 10で13%、n = 100で6%):それにもかかわらず、Stream APIバインディングは何かを消費します。 ただし、nが大きい場合、ストリーミングバージョンの動作は4〜8%速くなりますが、アルゴリズム的には同じように見えます。 測定を行わずに理論的にパフォーマンスを推測することは危険であるという別の確認。 JITコンパイラーの最適化、プロセッサーのキャッシング、分岐予測、およびその他の要因は、理論的には検討が非常に困難です。



非常に短い操作の場合、並列フローは大幅に遅くなると予想されます。 ただし、速度の増加は、n = 1000ですでに観察されています。この場合、完全な計算にはシリアルモードで約270μs、パラレルで75 sしかかかりません。 これは、 Stream Parallel Guidance (リンクについてはapanginに感謝します )と非常によく一致しています 。100μsよりも時間がかかるタスクには、並列化が理にかなっています。 nの値が大きい場合、馬の平行流:速度が18倍になります。 全体として、これは5nw倍のフロー数の増加と一致しています(1.7 / 0.8 * 8 = 17)。



ForkJoinPoolのオーバーヘッドは非常に小さいため、ミリ秒のタスクでも速度が向上します。 同時に、分割統治アルゴリズムは自然にパラレルストリームに移行するため、パラレルストリームはシーケンシャルストリームよりも大幅に高速化できます。 Stream APIは多くの人にscられていますが、並列処理はまだ未来です。



All Articles