読者です 読者をやめる 読者になる 読者になる

DoEvents Replacement (4)

DirectX .NET

ややこのネタで引っ張りすぎの感もありますので,そろそろ一旦区切りをつけたいと思います.
前回の調査(id:NyaRuRu:20040613#p1)からDoEventsをP/Invokeで置き換えることで確かにある程度のメモリ消費が抑えられることが分かりました.さて,このメモリ消費量は一般的なゲームのメインルーチンと比較してどの程度の影響を持つと考えられるでしょうか*1.ここでは一般的なアクション/シューティングゲームを想定し,ParticleSpritesサンプルを取り上げることにします.
さて,以下が実際ParticleSpritesサンプルをDoEventsとP/Invokeそれぞれで比較してみたものです.ただし回数は前回の1%である500フレームとなっています.
DoEvents使用時↓

DoEvents使用時
P/Invoke使用時↓

P/Invoke使用時


























ProcessMessageとDoEventsを500回呼び出した場合のメモリプロファイル
世代0 GC 世代1 GC 世代2 GC 内部メモリ割当量合計
DoEvents 46 回 4 回 3 回 57 kB
ProcessMessage(P/Invoke) 44 回 4 回 3 回 172 bytes

500回の結果ではありますが,確かに微妙な差は出るようでした.しかし,タイムライン表示で何より目につくのは,Particle構造体とPointVertex配列が常にマネージドヒープを頻繁に利用していることです.実際にこのサンプルを実行されたことがある方はご存じかと思いますが,このプログラムは多数のParticleが画面上で生成と消滅を繰り返すというものです.ソースを見ると1つ1つのParticleはフィールドも含めて完全な構造体で表現されています.構造体は値型のため,Particleに起因して毎フレームごとにマネージドヒープが消費されていくというのは少し奇妙に感じられる方もおられるかもしれません.実はこのサンプルではデータの管理にArrayListが用いていて,毎フレームのboxingの過程でマネージドヒープが消費されています.以下にこの現象を再現するコードを示しておきます.

using System;
using System.Collections;

class BoxingTest
{
    public struct Test
    {
        public int I;
    }

    static void Main(string args)
    {
        ArrayList array = new ArrayList( new Test[1] );
        // Test array = new Test[1];
        for( int i = 0; i < 100000; ++ i )
        {
            Test t = (Test) array[0];
            t.I++;
            array[0] = t; // ここで boxing される
        }
    }
}

boxingはParticleを固定長配列に確保するようにすることで回避することが可能です.
一方PointVertex配列は頂点バッファのLockで生成されていて,そのサイズは毎回8kBです.このサンプルは動的頂点バッファの例にもなっていて,毎フレーム頂点バッファをロックするためグラフのような多数のPointVertex配列が生成されることとなっています.
さてもしGC回数を気にしていて*2かつこのグラフを先に見ているのであれば明らかにParticle構造体の格納方法の変更とメモリ負荷の少ないLock方法の調査から始めることになるでしょう.もしこれらの影響を排除してプロファイルを取り直した場合,DoEventsの影響が今よりははっきりと現れるでしょうから,そこでDoEventsを置き換えるか判断することになるかと思います.
結論として私はDoEventsを問題視することそのものに反対はしませんが,その課程で「DoEventsを排除しさえすれば安全」と誤解する人が生まれることが気になります.実際.NET Framework 2.0でDoEventsの特性は変化するかもしれません.重要なのは正しい現象の理解に基づく行動であって,標語的な言葉を暗記して判断を外部に任せてしまう人が現れることに危険を感じます.もちろんそういう意味ではここで私が書いたことを信じないというのはとても正しい判断でしょう.時間測定ルーチンを自分で作ってデータをとってみたり色々なプロファイラで結果を比較したりして,各自でより深い理解とより素晴らしいアプリケーションを目指してください.
次回はDirectX 9.0 SDK Update (Summer 2004)が出た後に,新しいフレームワークで追試を行ってみたいと思います.

*1:前も書きましたが,これはパフォーマンスチューニング本来の流れと逆行しています.一般的にはゲームのメインルーチンで最も大きなボトルネックから順に注目していくため,調査対象は常に「大きな影響を持つ」部分ばかりとなります.苦労して調べた箇所が全体としてどの程度の意味を持つか分からないと,いうことは基本的にあってはならないことです.

*2:これ以前にそもそもGCが本当にゲームのパフォーマンスに影響を与えるのかの判断をしておく必要があります.実際はDoEventsなんかの小物を調べるよりよりそちらの調査の方が需要が高いのでしょうが明らかに調査量が膨れ上がるので今回はサボってしまいました.すんません.