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

効率の良い実行時バインディングとインターフェイス指向プログラミングでの boxing の回避テクニック (2)

.NET

id:NyaRuRu:20070216:p1 の続き.あるいは腐りかけてる書きかけの資料の処分.

上の Dispatcher<T> は,インターフェイス IFoo を実装している場合のみメソッド IFoo.Foo を呼び出しますが,このコードには T が値型のときに boxing が発生してしまうという欠点があります.これをリフレクションや IL Emit や LCG を用いて直接インスタンスメソッドを呼び出すように書き換えることもできますが,

LCG 版を書いておきます.
以下のコードは,型 T のオブジェクトに対し,MethodInfo で指定されたインスタンスメソッドを呼び出すコードを動的生成します.

public delegate void Proc<A0>(ref A0 a0);

public static Proc<T> MakeCall<T>(MethodInfo method) where T : struct
{
    DynamicMethod dm = new DynamicMethod(
            "Call",
            null,
            new Type[] { typeof(T).MakeByRefType() },
            typeof(T));
    Type t = typeof(T);
    MethodInfo minfo = method;
    if (method.DeclaringType.IsInterface && method.DeclaringType.IsAssignableFrom(t))
    {
        InterfaceMapping map = t.GetInterfaceMap(method.DeclaringType);
        for (int i = 0; i < map.InterfaceMethods.Length; ++i)
        {
            if (map.InterfaceMethods[i] == method)
            {
                method = map.TargetMethods[i];
                break;
            }
        }
    }
    ILGenerator gen = dm.GetILGenerator();
    ParameterBuilder pb = dm.DefineParameter(0, ParameterAttributes.In, "arg");
    gen.Emit(OpCodes.Ldarg_0);
    gen.EmitCall(OpCodes.Call, method, null);
    gen.Emit(OpCodes.Ret);
    return dm.CreateDelegate(typeof(Proc<T>)) as Proc<T>;
}

public static Proc<T> MakeConstrainedCallVirt<T>(MethodInfo method) where T : struct
{
    DynamicMethod dm = new DynamicMethod(
            "ConstrainedVirtCall",
            null,
            new Type[] { typeof(T).MakeByRefType() },
            typeof(T));
    Type t = typeof(T);
    ILGenerator gen = dm.GetILGenerator();
    ParameterBuilder pb = dm.DefineParameter(0, ParameterAttributes.In, "arg");
    gen.Emit(OpCodes.Ldarg_0);
    gen.Emit(OpCodes.Constrained, t);
    gen.EmitCall(OpCodes.Callvirt, method, null);
    gen.Emit(OpCodes.Ret);
    return dm.CreateDelegate(typeof(Proc<T>)) as Proc<T>;
}

MethodInfo には (暗黙に存在する this 引数を除いて) 引数をとらないメソッドを指定する必要があります.MethodInfo は型 T のインスタンスメソッドでも良いですし,型 T が実装するインターフェイス I のメソッドでも構いません.
一般的に,値型のオブジェクト T に対し,インターフェイスメソッドは直接呼び出せません.通常は T 型のオブジェクトを評価スタック上で一旦 boxing し,その上でインターフェイスメソッドを callvirt するという手順をとります.
一方上で示した MakeCall, MakeConstrainedCallVirt は,共に,インターフェイスメソッドの呼出し時にも型 T のオブジェクトの boxing は発生しません*1
前者はインターフェイスマップを解析して直接型 T のインスタンスメソッドを呼び出すことで boxing を回避します.後者は Generics 使用時にコンパイラが出力するのと同様,constrained prefix を利用し,CLR にディスパッチ処理を行わせることで boxing を回避します.
MakeCall, MakeConstrainedCallVirt は以下のように使用します.

public interface IFoo
{
    void Foo();
}
public struct Y : IFoo
{
    void IFoo.Foo()
    {
        Console.WriteLine("Foo!");
    }
}
static void Main(string[] args)
{
    Y y = new Y();

    MethodInfo m = Array.Find(typeof(IFoo).GetMethods(),
        delegate(MethodInfo info) { return info.Name == "Foo"; });

    Proc<Y> proc = MakeCall<Y>(m);
    Proc<Y> proc2 = MakeConstrainedCallVirt<Y>(m);

    // boxing は発生しない
    proc(ref y);
    proc2(ref y);
}

残念ながら,LCG が利用できない現行の Xbox360 版 Compact CLR では,この手法は使用できません.

*1:厳密には Object.GetType 等,一部のメソッドでは boxing が回避できないことがあります