第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
が今のままの仕様なら、遅延処理や並列化は意味がないのではないか
画像変換の遅延などしないで、画像変換が登録された時点で変換してしまう方がスピードもメモリも効率が良い
画像変換にSpliterator
のcharacteristics
のような特性を導入しないと、遅延処理や並列化の最適化は難しいと思った