並列処理が役立つ場合
処理量とデータサイズが左右します。
「並列処理が役立つ場合」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。
並列化にはコストがある
並列化すると、データの分割、タスクの割り当て、結果の結合によるオーバーヘッドが発生します。そのコストを上回る時間を節約できる場合にだけ効果があります。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long sum = IntStream.rangeClosed(1, 10_000_000)
.parallel()
.asLongStream()
.sum();
System.out.println(sum);
}
}要因1: データサイズ(N)
大きな N では、並列化の固定オーバーヘッドを償却できます。目安として、並列化の効果が出るのは数万個程度の要素からです。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long count = IntStream.rangeClosed(1, 5_000_000)
.parallel()
.filter(n -> n % 7 == 0)
.count();
System.out.println(count);
}
}要因2: 要素ごとの処理量(Q)
各要素を処理するコスト Q が重要です。要素ごとの処理が重い場合は、N が小さくても並列化の効果が得られます。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long primes = IntStream.rangeClosed(2, 200_000)
.parallel()
.filter(Main::isPrime)
.count();
System.out.println(primes);
}
static boolean isPrime(int n) {
for (int i = 2; (long) i * i <= n; i++)
if (n % i == 0) return false;
return true;
}
}N × Q で考える
有用な総処理量を N × Q と考えてみましょう。この積が大きいほど、並列化の効果が高くなります。N が非常に小さい場合や Q が軽い場合は、並列化する理由がほとんどありません。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
double sum = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.mapToDouble(n -> Math.sqrt(n) * Math.log(n + 1))
.sum();
System.out.println(sum);
}
}要因3: 分割可能性
安価かつ均等に分割できるデータソースは、並列化に適しています。たとえば配列、ArrayList、IntStream.range などです。LinkedList やイテレーターを基にしたソースは、分割が苦手です。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) list.add(i);
long even = list.parallelStream().filter(n -> n % 2 == 0).count();
System.out.println(even);
}
}適したソース: 配列と範囲
プリミティブ型の範囲はサイズが既知で、O(1) で分割できるため、並列処理のソースとして理想的です。
import java.util.stream.LongStream;
public class Main {
public static void main(String[] args) {
long sum = LongStream.rangeClosed(1, 20_000_000)
.parallel()
.sum();
System.out.println(sum);
}
}適さないソース: 反復型ジェネレーター
Stream.iterate は定義上、要素を逐次生成するため、要素が生成されるまで分割できません。並列処理のソースとしては適していません。
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
long count = Stream.iterate(1, n -> n + 1)
.limit(1_000_000)
.parallel()
.filter(n -> n % 2 == 0)
.count();
System.out.println(count);
}
}要因4: 安価な結合処理
並列化には、部分結果を安価に結合する方法が必要です。sum や count は簡単に結合できますが、ソート済みリストやツリーを多用するコレクターの構築では、結合に大きなコストがかかります。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int max = IntStream.rangeClosed(1, 5_000_000)
.parallel()
.map(n -> n % 1000)
.max()
.getAsInt();
System.out.println(max);
}
}I/O待ちの処理には使わない
並列ストリームは、ForkJoinPool 上で実行する CPU バウンドのタスクを対象としています。ブロッキング I/O は共有プールを枯渇させ、アプリケーション全体に悪影響を与えます。I/O には専用のエグゼキューターを使用してください。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long total = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.mapToLong(n -> (long) n * n)
.sum();
System.out.println(total);
}
}推測せず測定する
並列化に効果があるかを確実に知る方法は、現実的なデータを使ってベンチマークを実行することだけです。性能に関する直感は、しばしば間違っています。
import java.util.stream.LongStream;
public class Main {
public static void main(String[] args) {
long start = System.nanoTime();
long sum = LongStream.rangeClosed(1, 50_000_000).parallel().sum();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("sum=" + sum + " took ~" + ms + "ms");
}
}実践的なチェックリスト
次の条件をすべて満たす場合は、並列化を検討してください。
- N が大きい、または要素ごとの処理が重い。
- 分割可能なソース(配列、ArrayList、範囲)である。
- 安価で結合的な結合処理がある。
- CPU バウンドで、ブロッキング I/O がない。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
double avg = IntStream.rangeClosed(1, 10_000_000)
.parallel()
.mapToDouble(Math::sqrt)
.average()
.getAsDouble();
System.out.println(avg);
}
}確認問題
並列ストリームに最も適しているのは、どのような状況ですか。
まとめ
並列化が効果を発揮する条件を学びました。
- 効果は N × Q(データサイズと要素ごとのコストの積)に応じて大きくなります。
- 分割可能なソース(配列、ArrayList、範囲)が必要です。
- 安価で結合的な結合処理が必要です。
- CPU バウンドに限定し、ブロッキング I/O を避け、必ず測定してください。
よくある質問
「並列処理が役立つ場合」レッスンは無料ですか?
はい。「並列処理が役立つ場合」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。
「並列処理が役立つ場合」で何を学びますか?
処理量とデータサイズが左右します。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Java Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「並列処理が役立つ場合」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このJava Academyレッスンでコードを書いて実行できますか?
はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 並列ストリームの作成
- 並列処理が役立つ場合
- スレッド安全性と副作用
- よくある落とし穴