Toy problemsは役立たずか(Floydの問題・ネタバレ)


U = {√1, √2, …, √50}を2つのグループに分け、グループ毎の和を求める。和の差がなるべく小さくなるようなグループ分けを、制限時間10秒で見つけよ。

1881526917先日紹介したFloyd問題です。

10秒という制限時間で、2の49乗個の候補の中から最良のものを見つけ出すのは、1977年当時のコンピュータでは難しかったはずです。そこでKnuthは、以下のような工夫をしました。

  1. まず小数部分を近づけることを目指し、その後で整数部分を近づける。(この問題では結果オーライ。√40までの場合はうまくいかない。)
  2. I = {√1, √4, √9, √16, √25, √36, √49}は、和の整数部分にしか関与しないから後で考えることにする。
  3. Uを2つのグループAとBに分け、Aの部分集合とBの部分集合の合計の小数部分を、全体の合計の半分の小数部分に近づける。
  4. Aの部分集合の小数部分をすべて記録しておき、Bの部分集合に対して、上の条件に会うようなAの部分集合がすぐに見つかるようにする。
  5. 部分集合の生成にはグレイコードを使う。
  6. X = {√2, √8, √18, √32, √50}の組み合わせでできる和は16通りしかないことを利用する。同様に、Y = {√3, √12, √27, √48}の組み合わせでできる和は11通りしかないことも利用する。
  7. 集合をA = X ∪ Y ∪ {√5, √6, √7, √10, √11, √13, √14, √15}、B = U – I – Aとする。

今日のコンピュータは当時と比べればはるかに強力なので、これらの工夫のうちの1から4を採用するだけで、あとは富豪的なプログラムでも最良解を見つけるのに5秒とかかりません(コード)。本稿執筆時点のideone.comで約2.5秒(double)あるいは約2.8秒(long double)、私のデスクトップ(Core i7 950 3.07Ghz)のVisual C++で約1.3秒でした(いずれもシングルスレッド)。

4774157155Aの部分集合の記録には、C++11で導入されたunordered_multimapを使いました。キーAのサブセット和の小数部分8桁を、値はサブセットを表現する整数とサブセット和のpairです(この実装では、mapよりunordered_mapのほうがかなり速いです)。sizeAを20にしたり、keyを小数点以下8桁にすると速いのは、動かしてみてわかったことです。

OpenCLに対応するデバイスの列挙(C言語・Mathematica)


GPUをグラフィック処理ではなく汎用計算に利用しようというGPGPUのためには、CUDAかOpenCLを利用するのが一般的です。NVIDIA的には、CUDAはGPGPUの開発環境であり、プログラミング言語としてC for CUDAかOpenCLを選べる、つまりCUDAはOpenCLの上位概念らしいのですが、一般にはCUDAとOpenCLは対立するものとして認識されているような気がします。

CUDAはNVIDIAのGPUにしか対応していないのに対して、OpenCLはNVIDIAのGPUとAMDのGPU、IntelとAMDのCPUにも対応しているので便利です(対応していないGPUやCPUもあります)。

IntelのCPUとHD GraphicsでOpenCLを利用できるようにする、Intel SDK for OpenCL Applicationsの新版(2013)が出たので、ちょっと使ってみます。

OpenCLに対応したデバイスを列挙することで、複数のデバイスに対応しているというOpenCLの特徴を確認してみましょう。規格で定まっているわけではないようですが、複数のOpenCL環境をインストールしているときには、複数のOpenCLプラットフォームがOpenCLのAPIから認識できるようになるようです(後述の参考書p.71)。

実行するためには、以下のいずれかが必要です。

4844331728開発は、WindowsとGNU/Linux、Macのいずれでも可能です。株式会社フィックスターズ『OpenCL入門 1.2対応 マルチコアCPU・GPUのための並列プログラミング』(インプレスジャパン, 改訂新版, 2012)サポートサイト)などを参考にすると、開発環境を比較的簡単に構築できます。(参考:Visual Studio + Intel SDK for OpenCL

適当な開発環境を用意したら、以下のプログラムをビルド・実行します(RSSリーダーでは見られないかもしれません)。

OpenCLの実装は複数のプラットフォームを認識できるものになっているので、上のコードでは、プラットフォームを列挙しつつ、各プラットフォームが持つデバイスを列挙しています。

私のデスクトップPCでの実行結果はこんな感じです。プラットフォーム毎にデバイスが1つあるというわかりやすい構成です。

Platform: NVIDIA CUDA
CL_PLATFORM_VERSION: OpenCL 1.1 CUDA 4.2.1

  Device: GeForce GTX 580
  CL_DEVICE_VERSION: OpenCL 1.1 CUDA
------------------------------------------------------------------------------
Platform: Intel(R) OpenCL
CL_PLATFORM_VERSION: OpenCL 1.2

  Device: Intel(R) Core(TM) i7 CPU         950  @ 3.07GHz
  CL_DEVICE_VERSION: OpenCL 1.2 (Build 63463)
------------------------------------------------------------------------------

私のノートPCでの実行結果はこんな感じです。2番目のプラットフォームにはデバイスが2つあり、そのうち1つは1番目のプラットフォームのデバイスと同じという、わかりにくい構成になっています。

Platform: AMD Accelerated Parallel Processing
CL_PLATFORM_VERSION: OpenCL 1.2 AMD-APP (923.1)

  Device:       Intel(R) Core(TM) i7-3612QM CPU @ 2.10GHz
  CL_DEVICE_VERSION: OpenCL 1.2 AMD-APP (923.1)
------------------------------------------------------------------------------
Platform: Intel(R) OpenCL
CL_PLATFORM_VERSION: OpenCL 1.2

  Device:       Intel(R) Core(TM) i7-3612QM CPU @ 2.10GHz
  CL_DEVICE_VERSION: OpenCL 1.2 (Build 63463)

  Device: Intel(R) HD Graphics 4000
  CL_DEVICE_VERSION: OpenCL 1.1
------------------------------------------------------------------------------

プラットフォームについて得られる情報は、http://www.khronos.org/registry/cl/sdk/1.0/docs/man/xhtml/clGetPlatformInfo.htmlにまとまっています。

デバイスについて得られる情報は、http://www.khronos.org/registry/cl/sdk/1.0/docs/man/xhtml/clGetDeviceInfo.htmlにまとまっています。

Mathematicaなら、すべての情報を以下の2行で得られて便利です(参考:OpenCLInformation)。

Needs["OpenCLLink`"]
OpenCLInformation[]

マンデルブロ集合をOpenCLで描く(Mathematica)


CUDAを使ってマンデルブロ集合の描画を3桁速くする方法を以前紹介したのですが、同じことをOpenCLでやってみます。NVIDIAのGPUを搭載していないノートPCを使うことが多くなった自分用のメモでもあります。書き換え方は、OpenCLLink プログラミングで紹介されています。

書き換えたコードは以下の通りです(RSSリーダーでは表示されないかもしれません)。

最初に作成したコードをCore i7-3612QM 2.1GHz上で実行するのと比べて、ここで作成したコードをIntel HD Graphics 4000上で実行すると、3桁くらい速くなります。

Manipulateを使ってインタラクティブに描くコードは以下の通りです(RSSリーダーでは表示されないかもしれません)。

似たような話がMathematicaのマニュアルにも載っていますが、ここで書いたコードなら、描画の中心もインタラクティブに変わります。

realtime

CUDAを使ってマンデルブロ集合の描画を3桁速くする方法


追記:Mathematica 10で導入されたMandelbrotSetPlot[]は、GPUを使いませんがかなり速いです。この記事でやっていることは、以下のコードで再現できます。

追記:OpenCLの場合

描画が3桁速くなると、マンデルブロ集合をインタラクティブに楽しめるようになります。

はじめに

以前、Mathematicaでマンデルブロ集合を描く方法を紹介しました(「追悼、マンデルブロ博士」)。

Clear@f;
f[c_] := f[c, 0., 0]
f[_, _, 100] = -1;
f[c_, z_, i_] := With[{x = z^2 + c}, If[2 < Abs@x, i, f[c, x, i + 1]]]
DensityPlot[f[x + y I], {x, -2, 1}, {y, -1.5, 1.5}, PlotPoints -> 100]

描くのは確かに簡単だったのですが、「ここをもうちょっと拡大したい」という要望に応えるためのインタラクティブな仕掛けは、遅すぎて使えませんでした。「追悼記事なのに・・・」という思いがあったので、ちょっと改善することにしました。

目的

計算を高速化して、インタラクティブなマンデルブロ集合描画システムを作ります。

手法

(1)計算自体(上の例のf)高速化と(2)並列化で高速化します。(1)のために、再帰を反復に書き換えます。(2)のために、CUDAを使います。マンデルブロ集合の計算は、いわゆる「embarrassingly parallel」というやつなので、CUDAの練習に向いていると思います。

この記事のようにインタラクティブなものではありませんが、Mathematicaのマニュアルにもマンデルブロ集合の例が掲載されています。

条件

時間を計る条件は次のようにします。「有理数だと遅い」と思われるかもしれませんが、浮動小数点数を使うと、TableとParallelTableの結果が違ってしまう危険があることへの対応です。ちなみに、これはバグだと思うのですが、Wolfram社の回答は「バグでは無い」でした。

xMin = -2;
xMax = 1;
yMin = -3/2;
yMax = 3/2;
dx = (xMax - xMin)/300;
dy = (yMax - yMin)/300;

計算機環境(2013年3月更新)

  • CPU:Core i7 950(HTオフ)
  • 主記憶:12GB
  • GPU:NVIDIA GeForce GTX 580
  • OS:Windows 7 64 bit
  • Mathematica 9.0.1

結果

比較の基準

最初のバージョンをCUDAを使うバージョンで、計算時間を比較してみましょう。

最初のバージョンの実行時間を次のようなコードで計ると11.6秒でした。

CUDA

CUDAを使うバージョンの実行時間を次のようなコードで計ると0.003秒でした。4000倍くらい速くなることがわかります。

まあ、GPU無しの比較対象は最初に思いつくものですから、「まずGPU無しでどこまで速くなるのか追求してから」と思う方は、下の「おまけ」を読んでください。GPU無しでも最初のものより80倍くらいは速くできるので、それと比べれば、「GPUの効果は50倍くらい」というのが妥当なところでしょう。

うまくいかないときは、CUDAのための環境がちゃんとあることを確認してください。Visual StudioとCUDAがインストールされていればいいはずです(OpenCL版の方が簡単かもしれません)。私は、Visual Studio 2008 Professionalのデフォルトではx64のコンパイラがインストールされないために少しはまりました。本稿執筆時点でMathematicaが使うデフォルトのCUDA SDKは3.1で、これがサポートするのはVisual Studio 9 (2008)までだということにも注意が必要です(カスタマイズすればいいのでしょうが)。追記:2011年8月にCUDA SDK 4に対応したので、Visual Studio 2010がそのまま使えるようになりました。

リアルタイム描画

描画には、ArrayPlotを使うことにします。ListDensityPlotのほうが高度ですが、少し遅いのと、座標の扱いに問題があったため使えませんでした。Imageは速そうですが、やはり座標の扱いに問題がありました。

ユーザインターフェースはMathematicaのManipulateで構築します。

realtime

画像上をクリックすると、その場所を中心にして再描画します。Scaleで描画領域の広さを、Log[maxSteps]で反復の上限を変更できます。

ぬるぬる動くというわけにはいきませんが、「ここをもうちょっと拡大したい」という要望には十分応えられると思います。もっと速くしたい場合は、ArrayPlotをどうにかするということになるのでしょうが、もう少しコードを書かなければならないでしょう。

ところで、さすがにこれってCDFにしてもだめなんですよねえ。

おまけ(細かいことなので、これ以降は読まなくていいです)

追悼記事がダメだった理由

追悼記事がダメだった最大の要因は、DensityPlotにあったようです。DensityPlotは、下に示すように場所ごとにメッシュの細かさを自動調整してくれる便利な関数なのですが、この内部が並列化されていないため、あまり速くならないのです。

densityplot

CUDAなしでどこまで行くか

コンパイル+並列化

Mathematicaでは、機械精度の数を使うことに限定すれば、ユーザ定義関数をコンパイルできます。最初のバージョンをコンパイルし、並列化して実行すると30倍くらい速くなります。

fc = Compile[{{c, _Complex}, {z, _Complex}, {i, _Integer}},
   If[i == 100, -1, 
    With[{x = z^2 + c}, If[2 < Abs@x, i, fc[c, x, i + 1]]]], 
   CompilationTarget -> "C"];
DistributeDefinitions[fc];

time = AbsoluteTiming[
   result = ParallelTable[ fc[x + y I, 0, 0],
      {y, yMin, yMax, dy}, {x, xMin, xMax, dx}];][[1]];

time0/time

28.44619
反復

再帰を反復に書き直すともっと速く、80倍くらい速くなります。

hc = Compile[{{z, _Complex}},
   Module[{c = 0. + 0. I, i = 0},
    While[i++ < 100,
     c = c^2 + z;
     If[2 < Abs@c, Return@i]];
    Return@-1]];
DistributeDefinitions[hc];

time = AbsoluteTiming[
   result = ParallelTable[ hc[N@x + N@y I],
      {y, yMin, yMax, dy}, {x, xMin, xMax, dx}];][[1]];

time0/time

77.13116

ちなみに、この例ではコンパイル時に「CompilationTarget -> “C”」を付けてもあまり効果は無いようです。

CUDA無しでのリアルタイム描画

この方法を使ってリアルタイムに描画してみます。スケールや反復の上限もリアルタイムに変更できるように、少し修正します。

hc2 = Compile[{{xIndex, _Integer}, {yIndex, _Integer},
    {maxSteps, _Integer},
    {width, _Integer}, { height, _Integer},
    {xMin, _Real}, {xMax, _Real},
    {yMin, _Real}, {yMax, _Real}},
   With[{z = xMin + (xMax - xMin) xIndex/width
       + (yMin + (yMax - yMin) yIndex/height) I},
    Module[{c = 0. + 0. I, i = 0},
     While[i++ < maxSteps,
      c = c^2 + z;
      If[4 < Abs@c, Return@i]];
     Return@-1]],
   CompilationTarget -> "C"];
DistributeDefinitions[hc2];

width = 300; height = 300;
Manipulate[
 With[{
   xMin = center[[1]] - 10^scale, xMax = center[[1]] + 10^scale,
   yMin = center[[2]] - 10^scale, yMax = center[[2]] + 10^scale,
   maxSteps = IntegerPart[10^logMaxSteps]},
  result = ParallelTable[
    hc2[xIndex, yIndex, maxSteps, width, height, xMin, xMax, yMin, yMax],
    {yIndex, 1, height}, {xIndex, 1, width}];
  ArrayPlot[result, DataRange -> {{xMin, xMax}, {yMin, yMax}},
   DataReversed -> True, ColorFunction -> "Rainbow"]],
 {{center, {-0.5, 0}}, Locator},
 {{scale, 0, "Scale"}, -13, 1},
 {{logMaxSteps, 2, "Log[maxSteps]"}, 1, 3}]

実行結果は上に掲載したものと同じです。速いCPUがある場合はこれで十分なのかもしれませんが、私の環境では、「一応動くがストレスフル」という感じでした。

数独の平凡な解法(C言語)


数独の解き方をいくつか紹介したことがあります。

困ったことに、「どれも変態でわかりにくい、ふざけんな」という批判を受けたので、みんな大好きC言語で、平凡に書いてみました(実行結果)。

数独の平凡な解法(Mathematica)