Haskellからmysqlを実行する
Haskellからmysqlを叩きたくなったので調べていたのですが、かなり骨が折れる作業になりました。一筋縄に行かない問題が次から次へと…。しかしなんとか解決できたので、忘れないように覚書を残しておきます。
ビルドするのはThe HDBC-mysql packageです。パッケージ情報を見る限りGHC 7.4でビルド確認されているらしいのですが、恐らくテストされているのはLinux版で、MySQLも5.0.75を対象にしています。自分の環境はWindows Vista(x64)ですしMySQLは5.5.28なので、OS違いやバージョン違いの対応が中心でした。
ひとまず気をつけるべき点を8個ほど書きます。ここには手戻りしないように書いたので、この通りやってけば多分動きますが、またバージョンが変わったりしたらダメになると思います。なお自分の環境は次のような環境です。
- Windows Vista (x64)
- GHC 7.4.1
- MySQL 5.5.1 (64bit版)
- Connector/C 6.0.2 (32bit版)
- HDBC-mysql 0.6.6.1
HDBC-mysql 0.6.6.1をダウンロード
当然ですが、次のcabalコマンドは失敗します。
> cabal install HDBC-mysql
とりあえずHDBC-mysql 0.6.6.1をダウンロードして修正の準備に入ります。
Connector/C 6.0.2 (32bit版)をダウンロード
MySQLのインストールディレクトリには、libmysql.libとlibmysql.dllがあり、includeも入っています。これを使いたいところなのですが、自分の環境はMySQLが5.5.1の64bit版で、このdllがどうやら64bit版のものらしいです。あいにく現在Windowsで使えるGHCは32bit版です。したがって、このdllではGHCでビルドしたプログラムがlibmysql.dllと動的リンクできなくなります。というわけで、32bit版の接続用SDKを使います。
MySQLの公式サイトで、Connector/C 6.0.2のWindowsの32bit版のSDKを落とします。
もし32bit版を落とさないで作業を進めると、GHCでビルドしたプログラムを実行したときに「libmysql.dllが見つかりません」とか言われます(同じディレクトリにdllを置いてあるのに関わらず)。
mingw向けのインポートライブラリの作成
落としてきたConnector/Cには32bitのlibmysql.dllは含まれています。しかし、GHCがリンクするlibmysql.aがありません。含まれているのはMSVC向けのlibmysql.libです。そこで、これをGHCで利用できるようにするため、Haskellでiconvを参考にlibmysql.aを作成します。
> reimp -c -d libmysql.lib > dlltool -k -d libmysql.def -l libmysql.a
ここでdlltoolの-kを忘れずにつけておきます。これはmingwがlibmysql.dll内の関数を見つけるために必要になります。まず、.defファイルの中を見ると関数名の後ろに@8だの@12だの色々な数字がついているのが分かります。
libmysql.def ---------- LIBRARY "LIBMYSQL.dll" EXPORTS myodbc_remove_escape@8 mysql_affected_rows@4 mysql_autocommit@8 mysql_change_user@16 ...(略)...
この@数字はstdcallにすると勝手につくものです。mingwはstdcallな関数を見つけるとこの規約に沿って@数字をつけてコンパイルします。しかしlibmysql.dll内の関数名には@数字の修飾がついていません。このため、この-kを指定しないと、実行時に「mysql_autocommit@8が見つかりません」とか言われてしまいます。この-kというオプションは、この@以降の部分を無視してリンクすることが出来るようにするオプションです。
HDBC-mysql.cabalの修正
libmysql.aを作成したので、これをインストール時に利用することを指示します。これはHDBC-mysql.cabalで指定することができます。
HDBC-mysql.cabal ---------- library Exposed-modules: Database.HDBC.MySQL Other-modules: Database.HDBC.MySQL.Connection Database.HDBC.MySQL.RTS Build-Depends: HDBC >= 2.1.0, base >= 2 && < 5, bytestring, time, utf8-string ghc-options: -Wall + extra-libraries: mysql + extra-lib-dirs: C:\usr\work
ここで指定するextra-librariesには、mysqlと書くと、libmysql.aがリンクされるようになります。
追加インクルードディレクトリと利用ライブラリの修正
最初にcabal install HDBC-mysqlしたときに出たエラーメッセージの最後は次のようなものになっているはずです。
...(略)... setup.exe: The program mysql_config is required but it could not be found cabal: Error: some packages failed to install: HDBC-mysql-0.6.6.1 failed during the configure step. The exception was: ExitFailure 1
この「The program mysql_config is required but it could not be found」で名指しされているmysql_configですが、MySQLの色んな環境情報を取得する際に用いるプログラムです(includeディレクトリの情報など)。この情報はパッケージのビルド中にmysql.hのインクルード先を決めるのに利用されるのですが、mysql_configが見つからずビルドに失敗しています。
何故mysql_configが見つからないのかというと、MySQL 5.5.28のbinディレクトリにあるのはmysql_config.plというperlのスクリプトファイルで、これがWindowsでは検索に引っかからないためです。Linuxなら.plという拡張子もないだろうし、シェルが実行ファイルとみて実行させられるはずです。
というわけで修正します。HDBC-mysql-0.6.6.1/Setup.lhsの次の部分です。
Setup.lhs ---------- mysqlConfigProgram = (simpleProgram "mysql_config") { programFindLocation = \verbosity -> do mysql_config <- findProgramOnPath "mysql_config" verbosity mysql_config5 <- findProgramOnPath "mysql_config5" verbosity return (mysql_config `mplus` mysql_config5) } mysqlBuildInfo :: LocalBuildInfo -> IO BuildInfo mysqlBuildInfo lbi = do let mysqlConfig = rawSystemProgramStdoutConf verbosity mysqlConfigProgram (withPrograms lbi) ws = " \n\r\t" includeDirs <- return . map (drop 2) . split ws =<< mysqlConfig ["--include"] ldOptions <- return . split ws =<< mysqlConfig ["--libs"] return emptyBuildInfo { ldOptions = ldOptions, includeDirs = includeDirs } where verbosity = normal -- honestly, this is a hack
少し長いですが、ポイントはfindProgramOnPath "mysql_config"で、ここで"mysql_config.pl"見つからないのでエラーになっています。さて、ここでperlスクリプトを実行できるように書き換えても良いのですが、そんなことをしなくても結果だけ合ってればよいので、直接スクリプトファイルに結果をぶち込みます。
mysqlBuildInfoの戻り値を見ると、重要なのはldOptionsとincludeDirsの結果だけということが分かります。そこで、これを各環境の値に強引に書き換えてしまいます。
Setup.lhs ---------- + includeDirs <- return ["C:\\bin\\mysql-connector-c-noinstall-6.0.2-win32\\include"] + ldOptions <- return [] - includeDirs <- return . map (drop 2) . split ws =<< mysqlConfig ["--include"] - ldOptions <- return . split ws =<< mysqlConfig ["--libs"]
パスは自分が利用している場所に書き換えてください。
SOCKETのシンボルエラーを修正。
ここでrughc setup configure/runghc setup buildとすると色々とビルドエラーが出てきますので修正します。まず最初はこちら。
C:\Program Files\MySQL\MySQL Server 5.5\include/mysql_com.h:291:3: error: expected specifier-qualifier-list before 'SOCKET'
SOCKETはWindowsでお馴染みのSOCKETハンドルのことでしょう。msvcの場合はwindows.hをインクルードするとSOCKETが漏れなくついてくるのですが、mingwはついてこないようなので、これを使えるようにします。mingwでSOCKETが定義されているのはwinsock2.hですので、これを追加します。
Connection.hsc ---------- import qualified Database.HDBC.Types as Types import Database.HDBC.ColTypes as ColTypes import Database.HDBC (throwSqlError) + #include <winsock2.h> #include <mysql.h>
シグナルが色々と使えないのを修正
引き続きrunghc setup buildを続けると次のようなエラーが出ます。
Database\HDBC\MySQL\RTS.hsc: In function 'main': Database\HDBC\MySQL\RTS.hsc:38:5: error: 'SIGALRM' undeclared (first use in this function) Database\HDBC\MySQL\RTS.hsc:38:5: note: each undeclared identifier is reported only once for each function it appears in Database\HDBC\MySQL\RTS.hsc:39:5: error: 'SIGVTALRM' undeclared (first use in this function) Database\HDBC\MySQL\RTS.hsc:40:5: error: 'SIG_BLOCK' undeclared (first use in this function) Database\HDBC\MySQL\RTS.hsc:41:5: error: 'SIG_UNBLOCK' undeclared (first use inthis function) Database\HDBC\MySQL\RTS.hsc:46:5: error: 'sigset_t' undeclared (first use in this function)
さて、これらのエラーはRTS.hscのwithRTSSignalsBlockedという関数で利用されています。この関数はアクションを一つ引数に受け取り、SIGALRMやSIGVTALRMなどを無効にした状態でそのアクションを実行する、というアクションラッパーです。RTS.hscのコメントによれば、どうやらmysqlclientの関数はこれらのシグナルの割り込み後の復帰処理が上手く出来てないらしく、これらのシグナルを無効にして実行しないとならない場合があるようです。特に-threaded runtimeを利用したときに特によく発生するとのこと。
で、どうしようか悩んだ末に、とりあえずは無くても動くには動くので、まずはこれらのシグナルを全て無効にしてしまいます。そこで次のように修正。
RTS.hsc ---------- withRTSSignalsBlocked :: IO a -> IO a +withRTSSignalsBlocked act = act -withRTSSignalsBlocked act = runInBoundThread . alloca $ \set -> do - sigemptyset set - sigaddset set (#const SIGALRM) - sigaddset set (#const SIGVTALRM) - pthread_sigmask (#const SIG_BLOCK) set nullPtr - act `finally` pthread_sigmask (#const SIG_UNBLOCK) set nullPtr
sigset_tが使えないのを修正(追記)
さらに、mingwではsigset_tがsys/types.hに定義されているので、これを追加します。
RTS.hsc ---------- #include <signal.h> #include <sys/types.h>
mysql_real_connectの関数定義の修正
ここまでくるとbuild/installまで実行できるのですが、サンプルプログラムのビルドは通らないので修正はまだまだ続きます。
mysql_real_connectという関数がありますが、現在のConnector/Cのバージョンでは関数引数が変更されています。マニュアルを参考に修正していきます。
Connection.hsc ---------- foreign import stdcall unsafe mysql_real_connect :: Ptr MYSQL -- the context -> CString -- hostname -> CString -- username -> CString -- password -> CString -- database -> CInt -- port -> CString -- unix socket + -> CInt -- clientflag -> IO (Ptr MYSQL)
対応する呼び出し部分には適当にデフォルト値の0でも突っ込んでおきます。
Connection.hsc ---------- connectMySQL info = do mysql_ <- mysql_init nullPtr when (mysql_ == nullPtr) (error "mysql_init failed") case mysqlGroup info of Just group -> withCString group $ \group_ -> do _ <- mysql_options mysql_ #{const MYSQL_READ_DEFAULT_GROUP} (castPtr group_) return () Nothing -> return () withCString (mysqlHost info) $ \host_ -> withCString (mysqlUser info) $ \user_ -> withCString (mysqlPassword info) $ \passwd_ -> withCString (mysqlDatabase info) $ \db_ -> withCString (mysqlUnixSocket info) $ \unixSocket_ -> do rv <- mysql_real_connect mysql_ host_ user_ passwd_ db_ (fromIntegral $ mysqlPort info) + unixSocket_ 0 - unixSocket_ when (rv == nullPtr) (connectionError mysql_) wrap mysql_
関数定義をccallからstdcallにする
libmysql.dllの関数はstdcallです。ところがHDBC-mysqlは次のように書かれています。
Connection.hsc ---------- foreign import ccall unsafe mysql_get_client_info :: IO CString foreign import ccall unsafe mysql_get_server_info :: Ptr MYSQL -> IO CString foreign import ccall unsafe mysql_get_proto_info :: Ptr MYSQL -> IO CUInt ...(略)...
てなわけで、ccallを置換してstdcallに変換します。但し、一番最後にC標準関数のmemsetがあります。
foreign import ccall unsafe memset :: Ptr () -> CInt -> CSize -> IO ()
この関数はC呼び出し規約なので、これは変えてはいけません。
Foreign.newForeignPtrをForeign.Concurrent.newForeignPtrに変更する
これでいよいよリンクも通り実行できるようになるのですが、プログラムの終了時にSegmentation faultで死にます。原因を把握仕切れていないのですが、どうもForeign.newForeignPtrの周辺が怪しいようです。mysql_closeを直接に叩くと成功するのにForeign.newForeignPtrの自動解放に任せると死ぬようなので次のように変更します。
Connection.hsc ---------- - mysql__ <- newForeignPtr mysql_close mysql_ + mysql__ <- Foreign.Concurrent.newForeignPtr mysql_ (mysql_close mysql_)
Connection.hsc ---------- - foreign import ccall unsafe "&mysql_close" mysql_close - :: FunPtr (Ptr MYSQL -> IO ()) + foreign import stdcall unsafe mysql_close + :: Ptr MYSQL -> IO ()
明示的にdisconnectしない限り、mysql_closeが呼ばれないことに注意してください。
test.hsをビルド
自分が修正した点は以上です。これでテストプログラムを書いて試してみましょう。
test.hs ---------- import Control.Monad import Database.HDBC import Database.HDBC.MySQL main = do conn <- connectMySQL defaultMySQLConnectInfo { mysqlHost = "127.0.0.1", mysqlUser = "testuser", mysqlPassword = "password", mysqlDatabase = "testdb" } rows <- quickQuery' conn "SELECT 1 + 1" [] forM_ rows $ \row -> putStrLn $ show row
実行します。
c:\usr\work> test [SqlInt64 2]
レイのフラスタムパケット化はパストレに弱い?
Kirill Garanzhaの手法の続報で「パストレにおいて性能が出ない」ということが書かれたpptを見つけました。何故遅いのか理由の記述を探しているのですが詳しい記述が見当たらないなあ。
ただの推測ですが、フラスタムのパケット化を中心としたアルゴリズムなので、拡散反射面が多いと効率が急激に落ちるということなのかもしれないですね。また、Compression-Sorting-Decompressionの手続きはレイの方向が揃っていることが前提の処理なので、レイのハッシュ値が整列しない二次レイ以降はCompressionとDecompressionが全く利かないのかも。
やはりレイの性質に応じてアルゴリズムを変える必要があるのでしょうか。
レイの境界フラスタムの構築の仕方
Large Ray Packets for Real-time Whitted Ray Tracing, Ryan Overbeck Ravi Ramamoorthi William R. Mark
フラスタムの構築手法のところだけ。大きく分けて3通りの構築の仕方があります。
- 1次レイに関するフラスタムの構築
- 点光源のシャドウレイに関するフラスタムの構築
- 一般のレイ集合に関するフラスタムの構築
1次レイに関するフラスタムの構築
これは単純です。カメラの射影平面のタイル範囲の角のレイを引き伸ばしてフラスタムを作るだけですね。
点光源のシャドウレイに関するフラスタムの構築
シャドウレイは、点光源とシェーディング位置までの線分と考えることができます。1次レイの場合とは異なり自明な射影平面がないので、射影平面を適当に作ってしまいます。まずレイ集合の主軸を求め、次に主軸の垂直方向に射影平面を作ります。あとは1次レイの場合と同じですね。
一般のレイ集合に関するフラスタムの構築
直前の項の説明と同じことを、始点と終点の両側で行えばOKですね。主軸を求めて、その主軸に垂直な面を始点と終点側に作り、レイを投影します。投影範囲の角同士を繋げば完成です。
GPU向けのBVH構築の手法
次にこれを読んでいます。
Fast BVH Construction on GPUs, C. Lauterbach and M. Garland and S. Sengupta and D. Luebke and D. Manocha, EUROGRAPHICS 2009
http://luebke.us/publications/eg09.pdf
とりあえずSAHを利用する場合のみ読んでます。
主な並列化ポイントは2点あるようです。1つは、ノード分割を深さ優先ではなく幅優先でデータ並列処理すること。つまり並列可能なノード数は1、2、4、8、…と増えていきます。もう1つは、各ノード内でプリミティブを左右のノードに振り分ける際のSAH計算をデータ並列処理することです。ここでデータ並列に処理するのはプリミティブのほうではなくSAHの境界面です。つまり、SAHを評価する境界面がk個あるなら、k個スレッドを立てて1つずつ割り当てて、各スレッドで全プリミティブを処理します。
気になるのが、ルートノードにおける並列性です。最初の階層ではノード数は1つしかありませんから、並列化が期待できるのはk個のSAHを評価する境界面についてだけです。そしてスレッド数を増やすにつれて良くなるのは計算速度ではなくBVHの品質です。また、Waldの論文では「binの数はだいたい16程度で十分な品質が得られる」との説明があります。つまり「SAHを評価する境界面を16以上に増やしてもBVHの品質が良くなる可能性は低い」と推測されます。
階層ごとの処理時間のグラフでは、階層1、階層2と進むに従い半々に短縮されています。浅い階層のうちは階層が1つ増えたところで総プリミティブはほとんど減らないと思われるので(リーフノードが作られるまではプリミティブが減らないから)、並列可能なノード数が少ないことが結果に直結している形です。
ただ最後にも書いてありますが、このアプローチの場合はAABB-BVH木だけではなくOBBとかk-DOPのような構造に拡張することが容易です。高品質なBVHを作る代わりにレイの探索時間を削減できる可能性は期待できるかもしれませんね。(とはいえ結局シンプルなAABBが単純で速い、ということもあるのですが…)
リアルタイムでレイトレ用のBVHを構築するっていう論文
On fast Construction of SAH-based Bounding Volume Hierarchies, --- Ingo Wald, Proceedings of the 2007 Eurographics/IEEE Symposium on Interactive Ray Tracing
http://www.sci.utah.edu/~wald/Publications/2007///FastBuild/download//fastbuild.pdf
2007年の論文ですがこれは凄いなあ…。200Kポリゴンのモデルの高品質なBVHを20msで構築してしまうのだから凄まじい速度です。
レイトレを幅優先探索でGPUで処理するっていう論文
読み終えたので、忘れないようにメモ書きしておきます。GPUによる、高い並列性を持つレイトレーシングの手法の一つです。
Fast Ray Sorting and Breadth-First Packet Traversal for GPU Ray Tracing, Kirill Garanzha and Charles Loop
http://research.microsoft.com/en-us/um/people/cloop/garanzhaloop2010.pdf
レイのソート
このステージでは、メモリのコヒーレンシーだとか探索における分枝を減らすために、入力された大量のレイをソートします。
レイの原点と方向について仮想グリッドで離散化してハッシュ値を求め、ハッシュ値でソートします。このとき、ハッシュ値配列をソートする前後にランレングス圧縮と展開を導入します。もともとハッシュ値配列はほぼ整列されていますので、予めランレングス圧縮をかけてからソートしたほうが高速であるようです。とはいえ圧縮展開も無コストではないので、ここにも並列化向けの工夫がされているようです(SENGUPTA S., HARRIS M., GARLAND M.: Efficient parallel scan algorithms for GPUs 参照)
フラスタムの構築
このステージでは、ソートされたレイのリストから、コヒーレントなものを束ねて、フラスタムのリストを作ります。
フラスタム1つが含むレイの数は、だいたい128〜512といったところであるようです。当然、シーンにより最適値は変わります。
BVH幅優先フラスタム探索
「大量のフラスタム vs 大量のノード」という処理を、階層0、階層1、階層2、…とどんどん深く処理していき、最後の層になるまで続けるようです。
- フラスタムがあるだけ、(フラスタム,ルートノード) のペアの配列を作ります。(なので、かなりの数のペアが作られます)
- 全てのペアについて、フラスタム vs ノードの子供のAABB で衝突判定し、衝突した(フラスタム,子供ノード)のペアの配列を出力します。
- 最下層でないなら、次の階層へ行くために、出力された配列を元にして2へ戻ります。
- 最下層であるなら、出力された配列は(フラスタム,葉ノード)のペアの配列ですので、そこで終わります。
局所衝突判定
ここでフラスタムを展開して、レイと三角形の衝突判定を行います。
シェーダデバッガをONにするとフレームレートが下がる件について
ただの覚書ですが、原因不明のフレームレート低下に見舞われてしまったので記録しておきます。
①/Zi /Od コンパイルオプションを付けてシェーダをコンパイル
②DirectX Control Panelで「Enable Shader Debugging」をON
③PIXで対象プログラムを実行
これだけでエフェクトをソースレベルデバッグすることができます。すごい!
が、この設定をすると、Visual Studio上でのデバッグ起動時のフレームレートが極端に下がるようです。
デバッグ機能だからと割り切ればよいのですが、不思議なことに、プログラムをエクスプローラまたはPIXから起動する場合はフレームレートはほとんど低下しません。何故なのでしょうか…
とりあえず、シェーダデバッグ中以外はDirectX Control Panelの「Enable Shader Debugging」はOFFにしておくのが良いようです。