Unity用のプラグイン、Fast Shadow Receiverを開発したときの話。

Unityは基本的にシングルスレッドで動くゲームエンジンである。マルチコアプロセッサの場合、スキニングの処理はマルチスレッドで行なわれるのだが、スクリプトで書かれた処理はシングルスレッドが大前提である。

しかし、Unityエンジンが管理していないデータを扱うぶんにはマルチスレッドで処理しても良い。驚いたことにUnityのObjectはnullチェックをすることすらマルチスレッドで出来ないのである。Objectは比較演算子をオーバーライドしていて、nullチェックをするにもUnityエンジンにお伺いを立てているようである。 というわけで、メインスレッドでUnityのObjectから情報を集めておいて、その情報をワーカースレッドに渡してバックグラウンドで処理をすることになる。このときに便利なのがSystem.ThreadingのThreadPool

と思ったら、これが驚く程ヒドイものだった。手持ちのiPhone 4Sで試してみたら、このスレッドプールはスレッドを20個も作っていたのである。スレッドプールってハードウェアスレッドの数(SystemInfo.processorCount)だけスレッドを作って、余計なコンテキストスイッチや同期にかかるコストを抑えるために使うと思っていたので、この挙動にはビックリした。これが.NET Frameworkの仕様なのか、Monoの実装が悪いのかは良くわからない。Instrumentsを使ってFast Shadow Receiverのデモのパフォーマンスを計測した結果が下の画像(クリックすると拡大画像が表示されます)。ワーカースレッドの処理が全体の36.7%を占めていて、そのうちの22.6%が同期にかかっているコストなのである! 本来のするべき仕事は12.0%しかないので、同期処理が本来の仕事の2倍近くも時間を費していることになる。しかもメインスレッド側でも、スレッドプールにタスクを投げる処理に8.8%もの時間をかけている。

SystemThreadPoolSystemThreadPool2

というわけで、自分でスレッドプールを実装することにした。自作のスレッドプールを使ってパフォーマンスを計測した結果がこれ。22.6%かかっていた同期のコストは3.4%程度にまで減少。8.8%かかっていたタスクを投げる処理も2.0%に! これはスレッドプールにスレッドを2つ作成した場合の結果で、スレッドを1つだけにした場合、同期のコストは1.2%しかなかった。

NyahoonThreadPoolNyahoonThreadPool2

でもこのスレッドプールには制限があって、それはこのスレッドプールはスレッドセーフには出来ていないということ(笑)。単独のスレッドからしかタスクを投げることしかできないので注意してください。ソースコードはFast Shadow Receiverをご購入いただければもれなく付いてきますが、下にもソースコードを公開しておくので興味のある方は使ってみてください。ただし不具合による責任はとれません。ご使用の際はご自分で十分テストしてください。そしてもし不具合を見つけたらメールとかコメントでお知らせいただければ幸いです。

使い方はInitInstance()でシングルトンインスタンスを初期化して、QueueUserWorkItem()でタスクを投げるだけ。タスクの中で他のスレッドと同期を取ろうとするとデッドロックする可能性があるので、投げっぱなしでいずれ終了するタスクしか扱えません。メインスレッドでタスクの終了を待つのはもちろんOKです。

Fast Shadow Receiverをご購入いただいた方はMeshShadowReceiver.csの中でこのスレッドプールを使っているので参考にしてみてください。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

Anti Spam Code *