Grand Central Dispatchでお手軽並列処理

Grand Central DispatchMac OS X 10.6およびiOS 4に追加された新技術だ。

このGrand Central Dispatch(以下"GCD")は、プログラマー以外にその効用を説明することが難しい技術だ。漠然と「マルチコアプロセッサを効率的に使うための技術」と言っておくこともできるが、それじゃあなんでiOS 4に搭載されてんの?という話になってしまう。

これをプログラマー向けに説明するならば、次のような感じになるかと思う。

GCCを拡張してクロージャ的な機能を追加してみたら、非同期処理とか並列処理とかすげー書きやすくなったんで、ちょっとその辺りまとめてみました。

僕の個人的な意見としては、単にAppleコンパイラー技術者たちがC言語ファミリー(C/C++/Obj-C)にクロージャを追加したかっただけなんじゃないかな、と考えている。

とりあえずマンデルブロ集合

とりあえず何か重たい処理を題材にしてみようと思い、マンデルブロ集合を描くプログラムを用意した。そしてそれを動作画面で比較すると効果が分かりやすいかもしれないと思い、次のような動画を作ってみた。

左が普通にシングルスレッドで書いたプログラム、右がGCDを使って並列化したものだ。フラクタルな形状なので拡大する動きはイマイチ分かり難いということに、動画を作ってから気が付いた。ごめんなさい分かり難いです。まあ、注意深く見れば、右側がかなり高速化されているということが分かるでしょう……

ソースコード

シングルスレッド版のメイン部分は、こんな感じになっている。

  for (int y = 0; y < kImageSize; ++y) {
    double c_im = kMaxIm - y * kScaleIm;
    for (unsigned x = 0; x < kImageSize; ++x) {
      double c_re = kMinRe + x * kScaleRe;
      double re = c_re, im = c_im;
      for (int n = 0;;++n) {
        double re2 = re * re, im2 = im * im;
        if (n == kMaxIterations || re2 + im2 > 4) {
          pixels[x + y * kImageSize] = exp(1.0  / kMaxIterations * n) / 2.8 * 255;
          break;
        }
        im = 2 * re * im + c_im;
        re = re2 - im2 + c_re;
      }
    }
  }

よし、それじゃあ、これをGCDで並列化してみよう。こんな感じだ。


dispatch_apply(kImageSize, dispatch_get_global_queue(0, 0), ^(size_t y){
double c_im = kMaxIm - y * kScaleIm;
for (unsigned x = 0; x < kImageSize; ++x) {
double c_re = kMinRe + x * kScaleRe;
double re = c_re, im = c_im;
for (int n = 0;;++n) {
double re2 = re * re, im2 = im * im;
if (n == kMaxIterations || re2 + im2 > 4) {
pixels[x + y * kImageSize] = exp(1.0 / kMaxIterations * n) / 2.8 * 255;
break;
}
im = 2 * re * im + c_im;
re = re2 - im2 + c_re;
}
}
});

変更箇所は赤字にしておいた。というか、書き換えたのはたったの2箇所だけだ。

GCDに初めて触れる人にとって、まず気になるのは、1行目末尾の"^(size_t y){"の辺りの記述だと思う。これが例の「C言語クロージャ的なもの」であり、いわゆるラムダ式、あるいは無名関数のようなものだ。Appleはこれをblocks拡張と呼んでいる。

ここでは、このblockをdispatch_applyの引数に与えることで反復実行している。dispatch_applyによってグローバルキューに積み上げられたkImageSize個のblockは、GCDによって(可能であれば)コンカレントに実行される。

CPU使用率

アクティビティモニタでも確認してみた。これが元のプログラム。

GCDを使ったプログラムがこれ。

どの程度効率的に動いているかは分からないけれど、並列化されたうえで複数のコアを使い切っているのは確かなようだ。

感想

わりと何も考えずにほんの少しの変更だけで、いちおうのタスク並列化を実現できてしまうというのは、なかなかに素敵なことだと思う。

しかしながら、GCDが専らこのような並列化のための機能なのかというと、どうもそういう感じはしない。それでは前述のようにiOSに搭載する意味が無いからだ。

iOSにおけるGCDは、主に非同期処理の記述を簡単にするという目的で使われているようだ。今回の使い方にしてみても、「GCDは並列処理の記述を簡単にする」という解釈が可能だ。つまり、「記述を簡単にする」が主目的であり、その結果としてもたらされる並列処理だとか非同期処理だとかは、副次的なものであると考えることもできる。

そう考えると、これはやはり「Blocks拡張によって何が便利になるか」という視点に基づいて構築されたものなのだろうと思われてくる。あくまでもGCDは技術としてのラッピングであり、blocks拡張こそが真の目的であったのだ、と……