boxing チェッカー

(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);