SpriteBatchによる描画の並列化
About
ここではかなりキワモノ的な内容をあつかいます。.NetにはParallel.ForやParallel.Foreachといった、繰り返し構文を自動的にマルチスレッド(プロセス)にしてくれる、という大変便利な機能があります。これはXNAGameプログラミング内でも利用することができ、処理時間の短縮や、バックグラウンドでの事前計算が求められるゲームにおいては(シチュエーションにもよりますが)大変有用です。
しかしながら、大量のテクスチャなどをSpriteBatchを用いてレンダリングする場合に、Parallel.For(Foreach)は利用することができません。次のXBOXフォーラムでも取り上げられていました。
フォーラムの記事には掲載されていないし、ここで解説することも割愛するのですが、待合やLockなどを併用する場合でもSpriteBatchとParallel.Forを組み合わせて利用することはできません。できたとしても大変手間がかかり、その手間を施す負荷の方が大きくなる恐れがあると思われます。
そこで任意に並列化する場合はどうか、を検証してみました。つまり複数のSpriteBatchを用意して、それぞれのスレッドに1つずつSpriteBatchを与えて描画させる、というものです。
- WindowsGame_MultiSBatchTest.zip
- VisualStudio2010
- XNA4.0
SpriteBatchを複数利用できることの確認
ともあれまずは複数のSpriteBatchが利用できるのか、その場合の処理結果はどうなるのかを検証する必要があります。次のようなコードを用意して実験してみました。
spriteBatch_A.Begin(); spriteBatch_A.Draw(tex2d_Sample, Vector2.Zero, Color.White); spriteBatch_A.End(); spriteBatch_B.Begin(); spriteBatch_B.Draw(tex2d_Sample, new Vector2(0, 36), Color.Red); spriteBatch_B.End();
描画結果は次の通りで、内部的な処理はどうなっているかは置いておくとして、期待通りの結果が出力されることが確認できます。これで並列化処理を実験することができます。
TaskによるSpriteBatchの並列化
並列化による描画
並列化にはTaskクラスを利用することにします。次のようなコードで並列化を実験します。なお"Waitメソッド"を利用して処理の終了を待機しなければ、"SpriteBatchが適切に開始/終了されていない"であるとか、"null"であるといったエラーが返ります。
とりあえず10万回の描画を5万回ずつの2回に分けて実行します。
Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); spriteBatch_A.Begin(); spriteBatch_B.Begin(); //50,000回を2回で計100,000回 var t1 = Task.Factory.StartNew(() => { for (int i = 0; i < 50000; i++) { spriteBatch_A.Draw(tex2d_Sample, Vector2.Zero, Color.White); } }); var t2 = Task.Factory.StartNew(() => { for (int i = 0; i < 50000; i++) { spriteBatch_B.Draw(tex2d_Sample, Vector2.Zero, Color.White); } }); t1.Wait(); t2.Wait(); spriteBatch_A.End(); spriteBatch_B.End(); stopWatch.Stop(); Console.WriteLine("A : " + stopWatch.Elapsed);
従来手法の描画
比較用に従来通り10万回描画した結果を確認します。
Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); spriteBatch_A.Begin(); //100,000回 for (int i = 0; i < 100000; i++) { spriteBatch_A.Draw(tex2d_Sample, Vector2.Zero, Color.White); } spriteBatch_A.End(); stopWatch.Stop(); Console.WriteLine("B : " + stopWatch.Elapsed);
Result
実行環境は次の通りで、比較対象それぞれを数100回繰り返したのち、結果は適当な個所から5つデータを引っ張ったものです。
- 実行環境
- OS : Windows8
- CPU : Core(TM)i7-2600K, 3.40GHz
- Mem : 16.0GB
- VisualStudio2012, XNA4.0, DebugModeで実行
[並列化] A : 00:00:00.0120907 A : 00:00:00.0114800 A : 00:00:00.0116011 A : 00:00:00.0109548 A : 00:00:00.0131487 [従来] B : 00:00:00.0301377 B : 00:00:00.0301030 B : 00:00:00.0296511 B : 00:00:00.0299074 B : 00:00:00.0302817
結果を見てわかる通り、一応の高速化には成功しています。ここでは10万回の描画というあまり現実的ではない方法で処理負荷をかけていますが、現実的には描画数はせいぜい数万程度でしょうし、条件判定や描画位置・回転の計算(それにともなったインスタンスの生成)などによって、もう少しパフォーマンスに違いが出るはずです。
なにより下手な条件分岐の1つや2つよりも、Drawコール一回の方が処理負荷がかかるので、安易に並列化したからと言ってパフォーマンスが改善するか、と言えばそうではなく、あくまでシチュエーションによるでしょう。これはゲームに限らずプログラムに共通ですね。今回の場合でいえば、描画回数や条件判定などによる負荷が小さければ、並列化する意味はなく、むしろスレッドの用意と待合および破棄による負荷の方が大きくなってしまう場合がある、ということです。
「条件判定処理が複雑で、しかもそれによってDrawコールが発生しない場合が生じる」であるとか、「あらかじめ描画するテクスチャ(スプライト)の数が決まっていて、それが相応に数が多い」などの場合には並列化処理が有用になる可能性があるでしょう。ここではあつかっていませんが、RenderTarget2Dに並列化して(バックグラウンドで)書き込んでおいて後で参照する、であるとかそういう応用も考えられます。描画時にSpriteSortMode.Immediateを利用するであるとか、そういった"ふつうの工夫をした後で"、まだパフォーマンスを改善する必要があるキワモノアプリケーションの場合には、SpriteBatchの並列化試す価値があるかもしれません。