はじめに
前回の記事「.NETでマンデルブロ集合を描く」では、20年以上前にBASICで書いたマンデルブロ集合描画プログラムの.NETによる再実装を試みました。出来上がったアプリケーションはマンデルブロ集合の全体像を2秒弱で描画できたのですが、僕としては正直期待外れ。当時例えば10時間(=36,000秒)かかったとして、2万倍速くなっているなら1.8秒で描画できる勘定にはなります。だけどね、当時と比べてみればCPUのレジスタ幅は4倍、クロックは4千倍近いし、メモリに至っては数十万倍、加えて当時はBASICインタプリタで動いてたわけで、両者の速度比は2万倍どころじゃないはずなんですよ。
そこで今回は高速化をねらいます。現在(2009年12月)Visual Studio 2010と共にβ2がリリースされている.NET Framework 4.0では並列化をサポートするライブラリ:TPL(Task Parallel Library)が大きな売りの一つです。このTPLを使ってみましょうか。ついでにVC++10.0での並列化ライブラリ:PPL(Parallel Patterns Library)も。
速くないのはナゼなんだろう
このアプリケーション、僕の所属するわんくま同盟の勉強会で軽く紹介しました。デモのお披露目とともに計算と描画のからくりをざっくり説明したところ、会場から「GDIで点打ってんの? それじゃ処理時間の多くは描画に食われてんじゃない?」とのご意見。……なるほど。ちょっと調べてみましょう。
ものは試しにBitmapに点を打つ、すなわちSetPixelしている箇所をコメントアウトし、コンパイル/実行しました。その結果BackgroundWorkerの開始から終了までの所要時間が今まで1,600[ms]だったのが1,000[ms]に短縮されました。3割以上の時間をSetPixelに費やしているわけです。
マンデルブロ集合の計算はマルチスレッドと非常に相性がいいはずなんです。というのも、ある点に対する計算は他の点と完全に独立していますから、他のスレッドとの同期やリソースの排他制御の必要がなく、各スレッドは無駄な待ち合わせなしにフルスピードでぶん回ることができます。
ところがそのスレッド内でBitmapにSetPixelしていると話が違ってきます。Bitmapは複数のスレッドから同時にアクセスできませんから、そこにはBitmapというリソースの排他制御、要するに無駄な待ち合わせが生じます。待ち時間が計算そのものに要する時間に比べて十分に小さいならまだしも、先ほどのSetPixelを端折る実験によればBitmapへのSetPixelは相当に重い処理/時間のかかる処理と考えられます。
前回用いたユースケース風クラス関連図を再掲します(図1)。計算コビト(MandelbrotPlotter)と絵描きコビト(MainFrame)の両者が参照し、お互いの仲立ちをしている方眼紙(SectionPaper)はその内部にBitmapを抱えています。従って絵描きコビトが描画のためにBitmapnにアクセスしている間、計算コビトは計算の結果をBitmapに反映させることができません。
また、今回企んでいる並列化による高速化では、計算コビト内の計算ルーチンを複数個立ち上げ、複数の計算を同時に行って高速化をねらうつもりなのですが、計算ルーチンの出力先がBitmapである限り本来独立/並行動作できるはずの複数の計算ルーチンが待ち合わせを行わざるを得なくなります。