CUBは子供の白熊

Java SE 8 実践プログラミングの練習問題を解く

第3章 ラムダ式を使ったプログラミング : 問題 15 : 画像変換の並列化

問題

LatentImageクラスに操作の並列化機能を追加せよ

解答

LatentImageクラスの Nested Class であるTransformedPixelReaderはスレッドセーフではないので、スレッドセーフな派生クラスを定義する

まずTransformedPixelReaderのフィールドを全てprivateからprotectedに変更する

■ スレッドセーフな変換画像の Pixel Reader

private static class ParallelTransformedPixelReader extends TransformedPixelReader {
    /**
     * コンストラクタ
     * @param transformer  画像変換
     * @param original     変換元画像に対応する Pixel Reader
     * @param width        変換元画像の幅
     * @param height       変換元画像の高さ
     */
    public ParallelTransformedPixelReader(ImageTransformer transformer, PixelReader original,
        int width, int height
    ) {
        super(transformer, original, width, height);
    }

    /**
     * コンストラクタ
     * 指定された画像に対応するスレッドセーフな Pixel Reader
     * @param image  画像
     */
    public ParallelTransformedPixelReader(Image image) {
        super(null, null, (int)image.getWidth(), (int)image.getHeight());
        // キャッシュに全て展開する
        PixelReader reader = image.getPixelReader();
        for (int x = 0; x < cache.length; x++) {
            for (int y = 0; y < cache[x].length; y++) {
                cache[x][y] = reader.getColor(x, y);
            }
        }
    }

    /**
     * 指定された座標の色
     * @param x  X座標
     * @param y  Y座標
     * @return
     */
    public Color getColor(int x, int y) {
        // 画像変換がないケースは、展開済みなのでキャッシュから返す
        if (transformer == null) {
            return cache[x][y];
        }
        // ラインごとに排他制御して画像変換 … 本当はピクセルごとと言いたいが
        synchronized (cache[x]) {
            if (cache[x][y] == null) {
                cache[x][y] = transformer.apply(x, y, original);
            }
            return cache[x][y];
        }
    }
}

次にLatentImageクラスだが、toImageメソッドは従来どおり並列化しないで、並列処理で画像変換するtoParallelImageメソッドを追加する

そのため、フィールドPixelReader transformedは廃止してList<ImageTransformer> pendingOperationsに戻す

LatentImage#toImageメソッド

/**
 * 全ての画像変換を適用した画像の取得
 * @return  全ての画像変換を適用した画像
 */
public Image toImage() {
    // 画像変換がないときは元画像を返す
    if (pendingOperations.isEmpty()) {
        return in;
    }
    // 変換済み画像の Pixel Reader の連鎖を構築
    PixelReader transformed = in.getPixelReader();
    for (ImageTransformer f : pendingOperations) {
        transformed = new TransformedPixelReader(f, transformed, width, height);
    }
    // 画像変換
    WritableImage out = new WritableImage(width, height);
    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            out.getPixelWriter().setColor(x, y, transformed.getColor(x, y));
        }
    }
    return out;
}

LatentImage#toParallelImageメソッド

/**
 * 全ての画像変換を並列に適用した画像の取得
 * @return  全ての画像変換を適用した画像
 */
public Image toParallelImage() {
    // 画像変換がないときは元画像を返す
    if (pendingOperations.isEmpty()) {
        return in;
    }
    // 変換済み画像の Pixel Reader の連鎖を構築
    PixelReader transformed = new ParallelTransformedPixelReader(in);
    for (ImageTransformer f : pendingOperations) {
        transformed = new ParallelTransformedPixelReader(f, transformed, width, height);
    }
    PixelReader reader = transformed;  // final のおまじない
    // 画像変換 … Pixel Writer はスレッドセーフじゃないので、結果はオフスクリーンに
    Color[][] screen = new Color[width][height];
    try {
        int processorCount = Runtime.getRuntime().availableProcessors();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int n = 0; n < processorCount; n++) {
            int x0 = (n * width) / processorCount;
            int x1 = ((n + 1) * width) / processorCount;
            pool.submit(() -> {
                for (int x = x0; x < x1; x++) {
                    for (int y = 0; y < height; y++) {
                        screen[x][y] = reader.getColor(x, y);
                    }
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MINUTES);
    } catch (InterruptedException ex) {
        return in;
    }
    // オフスクリーンから画像を生成
    WritableImage out = new WritableImage(width, height);
    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            out.getPixelWriter().setColor(x, y, screen[x][y]);
        }
    }
    return out;
}

おまけ

synchronizedを入れただけで、私のマシンでは20%遅くなった
私のマシンのコア数は4つなので、並列化しても挽回ならず結局遅くなってしまった(残念…)

LatentImageが今のままの仕様なら、遅延処理や並列化は意味がないのではないか
画像変換の遅延などしないで、画像変換が登録された時点で変換してしまう方がスピードもメモリも効率が良い

画像変換にSpliteratorcharacteristicsのような特性を導入しないと、遅延処理や並列化の最適化は難しいと思った