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

boxing チェッカー

.NET XNA

(2008年12月8日追記) オペランドが OperandType.ShortInlineVar のときの読み取り処理が抜けていたバグを修正しました.



XNA で問題となる,頻繁な GC の原因のひとつに,隠れた boxing あります.変なところに嵌りこんでいる boxing コードは意外と探すのが大変です.だったら生成されたアセンブリをディスアセンブルし,box opcode を探してしまえということでプロトタイプを作ってみました.コンソールアプリケーションなので,必要な方は GUI をかぶせてお使い下さい.

コード解説

やっつけツールなのであちこち手を抜いていますが,処理の大まかな流れは DumpBoxingIL を読むことで把握できます.

static void DumpBoxingIL(FileInfo targetFile)
{
    var targetAsm = Assembly.LoadFrom(targetFile.FullName);

    SetupAssemblyResolver(targetAsm, targetFile.Directory);

    var scanResult = from method in targetAsm.GetAllMethods()
                     let opcodes = method.GetOpCodes().ToArray()
                     let boxingPoints = from opcodeInfo in opcodes
                                        where opcodeInfo.OpCode == OpCodes.Box
                                        select opcodeInfo
                     where boxingPoints.Any()
                     select new { Method = method, Opcodes = opcodes, BoxingPoints = boxingPoints.ToArray() };

    foreach (var item in scanResult)
    {
        Console.WriteLine(item.Method.ReflectedType + "." + item.Method.Name);
        foreach (var opcodeInfo in item.Opcodes)
        {
            if (opcodeInfo.OpCode == OpCodes.Box)
            {
                Console.ForegroundColor = ConsoleColor.Red;
            }
            Console.WriteLine("    IL_{0:X4}\t{1}", opcodeInfo.PC, opcodeInfo.OpCode);
            if (opcodeInfo.OpCode == OpCodes.Box)
            {
                Console.ForegroundColor = ConsoleColor.White;
            }
        }
        Console.WriteLine();
    }
}

SetupAssemblyResolver は,アセンブリルックアップの設定を行っています.Xbox 360 用のアセンブリは,XNA GSE のフォルダ以下にある BCL アセンブリを使用するので,その配慮も必要になります.
boxing opcode のスキャンですが,クエリ式に書かれている処理のうち,BCL に含まれず,今回新たに書いたのは次の 2 つです.

  • Assembly インスタンスに含まれる全てのメソッドを列挙する GetAllMethods
  • MethodInfo インスタンスの IL コードを OpCode と Program Counter の対に分解する GetOpCodes

GetAllMethods は実質的にはただの多重ループです.

static IEnumerable<MethodInfo> GetAllMethods(this Assembly assembly)
{
    var methods = from module in assembly.GetModules()
                  let globalMethods = module.GetMethods()
                  let typeMethods = from type in module.GetTypes()
                                    let bindingFlag = BindingFlags.NonPublic
                                                    | BindingFlags.Public
                                                    | BindingFlags.Instance
                                                    | BindingFlags.Static
                                                    | BindingFlags.GetProperty
                                                    | BindingFlags.SetProperty
                                    from typeMethod in type.GetMethods(bindingFlag)
                                    select typeMethod
                  from method in Enumerable.Concat(globalMethods, typeMethods)
                  select method;
    return methods;
}

GetOpCodes は巨大な switch case です.MethodBody.GetILAsByteArray() で得られたバイト列を先頭から順に調べています.
CIL の OpCode 一覧は,System.Reflection.Emit.OpCodes クラスに静的フィールドとして定義されています.
一覧および,short 値からの逆引き辞書は次のように作ることができます.

var opcodes = typeof(OpCodes).GetFields()
                             .Select(field => (OpCode)field.GetValue(null))
                             .ToArray();
var opcodedict = opcodes.ToDictionary(op => op.Value);