0Pricing
Java Academy · レッスン

並列処理が役立つ場合

処理量とデータサイズが左右します。

「並列処理が役立つ場合」は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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 並列ストリームの作成
  2. 並列処理が役立つ場合
  3. スレッド安全性と副作用
  4. よくある落とし穴
← Java Academyに戻る