Slide 1

Slide 1 text

Fork/Join フレームワークを 効率的に正しく使いたい JJUG night seminar LT / 2019-09-06 KOMIYA Atsushi

Slide 2

Slide 2 text

@komiya_atsushi

Slide 3

Slide 3 text

Fork/Join フレームワーク

Slide 4

Slide 4 text

slideshare.net/skrb/forkjoin-frameworklambda

Slide 5

Slide 5 text

どういう処理に使える・適しているのか? ● 処理の特性 ○ CPU-intensive である ○ 並列化できる ○ 小さな問題に再帰的に分割できる ● 例 ○ 分割統治法に基づく処理 ■ マージソート、クイックソート ○ 数値計算、シミュレーション ■ 単純計算をひたすら繰り返す類のもの

Slide 6

Slide 6 text

基本的な使い方

Slide 7

Slide 7 text

Fork/Join フレームワークの使い方 1. ForkJoinTask を継承したクラスで処理を実装する ○ ForkJoinTask よりも RecursiveAction や RecursiveTask を継承した方が実装しやすい 2. ForkJoinPool オブジェクトを用意する ○ ForkJoinPool.commonPool() ○ new ForkJoinPool(int parallelism) 3. ForkJoinPool#invoke(task) で処理を実行する

Slide 8

Slide 8 text

public class QuickSortTask extends java.util.concurrent.RecursiveAction { private final double[] values; private final int start, end; public QuickSortTask(double[] values, int start, int end) { this.values = values; this.start = start; this.end = end; } @Override protected void compute() { if (end - start < 30) { insertionSort(); return; } int p = partition(); invokeAll(new QuickSortTask(values, start, p), new QuickSortTask(values, p, end)); } private void insertionSort() { ... } private int partition() { ... } }

Slide 9

Slide 9 text

効率よく Fork/Join フレームワークを 使う

Slide 10

Slide 10 text

dl.acm.org/citation.cfm?id=2647511

Slide 11

Slide 11 text

ベストプラクティス ● タスクの粒度を慎重に選ぶ ● ForkJoinTask オブジェクトのサイズを (byte 的な意味 で) 小さく保つ ● 不要な fork を避ける ● リソース競合を避ける

Slide 12

Slide 12 text

デザインパターン ● Linked Subtasks ○ 不定個数のサブタスクを保持する方法についてのパターン ● Leaf Tasks ○ サブタスクに処理を分割をするタスクと、実際に計算などの処理を するタスクの実装クラスを分ける ● Avoid unnecessary forking ○ 不必要な ForkJoinTask#fork() の呼び出しを避ける ● Sequential Cutoff ○ タスクの粒度が小さくなったところでサブタスクへの分割を止めて (並列処理せずに) 直列的に計算をする

Slide 13

Slide 13 text

アンチパターン ● Heavyweight Splitting ○ サブタスクへの分割でオーバーヘッドの大きい処理をしてし まう ● Heavyweight Merging ○ サブタスクの処理結果をマージする際にオーバーヘッドの大 きい処理をしてしまう ● Inappropriate Sharing of Resources among Tasks ○ 並列実行されるタスク間で不適切なリソース共有をしてしまう

Slide 14

Slide 14 text

bit.ly/k11i-fork-join

Slide 15

Slide 15 text

速度パフォーマンスにどれほど影響があるの? ● デザインパターン / アンチパターンの一部について、速 度的な影響を jmh で測定 ● デザインパターン ○ Sequential Cutoff ● アンチパターン ○ Heavyweight Merging ○ Inappropriate Resource Sharing

Slide 16

Slide 16 text

bit.ly/k11i-fj-bench

Slide 17

Slide 17 text

Design pattern: Sequential Cutoff

Slide 18

Slide 18 text

Design pattern: Sequential Cutoff ● Fork/Join のタスクで処理すべきデータの大きさ (e.g. 配列やリストのサイズ) に着目する ○ Fork/Join で再帰的にデータを分割する ● データの大きさが適度に小さくなったら、再帰的な分割 を止めて具体的な処理を実行する ● 分割を止める「閾値」の調整がパフォーマンスを左右す る

Slide 19

Slide 19 text

例: マージソート ● 乱数生成された長さ 30 万の double の配列を マージソートでソートする ○ 配列の大きさが閾値以下となったら分割を止めて Arrays.sort() でソートする ● 10 / 100 / 1,000 / 10,000 / 100,000 の 5 つの閾値を 適用して評価する ● 1秒あたりの処理回数を計測する

Slide 20

Slide 20 text

public class MergeSortTask extends RecursiveAction { private static final int THRESHOLD = 1000; private final double[] x, work; private final int start, end; public MergeSortTask(double[] x, double[] work, int start, int end) { this.x = x; this.work = work; this.start = start; this.end = end; } @Override protected void compute() { int n = end - start; if (n > THRESHOLD) { int mid = start + (n >>> 1); invokeAll(new MergeSortTask(work, x, start, mid), new MergeSortTask(work, x, mid, end)); merge(mid); } else { Arrays.sort(x, start, end); } } private void merge(int mid) { ... } }

Slide 21

Slide 21 text

ベンチマーク測定結果

Slide 22

Slide 22 text

ベンチマーク測定結果 タスクの粒度が小さすぎると並列処理の オーバーヘッドの影響が出て パフォーマンスが悪化する

Slide 23

Slide 23 text

Anti-patterns: Heavyweight Merging, Inappropriate Resource Sharing

Slide 24

Slide 24 text

Anti-pattern: Heavyweight Merging ● サブタスクの処理結果をマージする際に、不必要に オーバーヘッドの大きい操作をしてしまう ○ 特に List#addAll() などのコレクションの操作でオーバー ヘッドが生じやすい

Slide 25

Slide 25 text

Anti-pattern: Inappropriate Resource Sharing ● タスク間で共有するリソースの排他制御 (ロック) が適 切でない ○ ロックが不要である、ロック粒度が粗い、リソース競合しうる のにロックしていない… など

Slide 26

Slide 26 text

例: double の配列から k 以上の値を抽出する ● 乱数生成された長さ 50 万の double 配列から k 以上 の値を抽出して最終的に List として出力す る ● Fork/Join フレームワークにより、タスクが担当 すべき配列上の範囲を再帰的に分割する ○ 一定の大きさになったところで、範囲内にある k 以上の値を 抽出してサブタスクの処理結果とする

Slide 27

Slide 27 text

例: double の配列から k 以上の値を抽出する ● Heavyweight Merging ○ List をタスクの戻り値とする ○ 親のタスクにおいて、サブタスクの戻り値を List#addAll(Collection) でマージする ● Inappropriate Resource Sharing ○ Collections.synchronizedList() で包まれた List をグローバルなリソースとして扱う ○ この List に抽出された値を List#add() する

Slide 28

Slide 28 text

例: double の配列から k 以上の値を抽出する ● Baseline ○ タスクの結果を木構造で保持する ■ 葉ノードにおいて抽出された値を List のまま で保持する ○ Fork/Join フレームワークによる並列処理を終えた後に、木 構造を辿って抽出結果を一つの List に集約する

Slide 29

Slide 29 text

ベンチマーク測定結果

Slide 30

Slide 30 text

ベンチマーク測定結果 不適切なリソース共有をしたときの パフォーマンス悪化が顕著

Slide 31

Slide 31 text

ベンチマーク測定結果 シングルスレッドで 処理するよりも悪い

Slide 32

Slide 32 text

ベンチマーク測定結果 Stream#parallel() による並列化だとベ ストパフォーマンスは得られない

Slide 33

Slide 33 text

まとめ

Slide 34

Slide 34 text

Fork/Join フレームワークを効率的に使うには? ● リソースの共有方法には十分気をつける ○ Inappropriate Resource Sharing ● オーバーヘッドの大きい処理は避ける ○ Heavyweight Splitting/Merging ● 適切なタスクの粒度を見定める ○ Sequential cutoff ● 実装次第では Stream#parallel() よりもよい パフォーマンスを達成できることも!

Slide 35

Slide 35 text

Thanks!