効率の良い実行時バインディングとインターフェイス指向プログラミングでの boxing の回避テクニック
id:NyaRuRu:20070214:p1 の続き,のつもりで書いていますが続けて読めるかは不明.
.NET プログラミングで,ある型 T に依存した抽象基底クラスやインターフェイスについて,利用者には T が緩い制約しかもたないように見せつつ,実装者は T の型にいくつか具体的な想定をして実装を行いたいことがあります.
これは例えば T が特定のインターフェイスを実装している場合や,特定の属性でマークされている場合に,より最適化された処理や,特殊な割り込み処理を行いたい場合を想定しています.
一般的にこれはリフレクションを用いて実現することになりますが,何度もリフレクションを行うのは効率が悪いので,初回実行時に T に応じた実装をはき出してしまうことを考えます.こうすることで,2回目からの実行では JIT コンパイルされた実行コードが使用されるようになります.
このような目的に使える実行時コード生成の方法がいくつかあります*1.
- IL Emit による動的コード生成
- Lightweight Code Generation (LCG) による動的メソッド生成 (.NET 2.0 以降)
- Expression Tree による構文木操作と動的メソッド生成 (.NET 3.5 以降予定)
これらに加え,ジェネリクスも,ある種の実行時コンパイル技術として利用することができます.出発点として以下のようなコードを考えてみましょう.
public interface IFoo { void Foo(); } public class Dispatcher<T> { public static void DoSomething(T arg) { // T が IFoo をサポートするときのみメソッドを呼び出す // 注) この判定が偽でも arg が IFoo をサポートしていることはある if( typeof(IFoo).IsAssignableFrom(typeof(T)) ) { (arg as IFoo).Foo(); } } } // 偽物 public struct X { public void Foo() { Console.WriteLine("foo?"); } } // IFoo を実装する本物 public struct Y : IFoo { public void Foo() { Console.WriteLine("Foo!"); } } public static class Test { public static void Main() { Dispatcher<X>.DoSomething(new X()); Dispatcher<Y>.DoSomething(new Y()); // 実行結果: Foo! } }
上の Dispatcher<T> は,インターフェイス IFoo を実装している場合のみメソッド IFoo.Foo を呼び出しますが,このコードには T が値型のときに boxing が発生してしまうという欠点があります.これをリフレクションや IL Emit や LCG を用いて直接インスタンスメソッドを呼び出すように書き換えることもできますが,ここではセオリー通りジェネリクスを用いて boxing を回避してみましょう.Dispatcher<T> を以下のように書き換えます.
public class Dispatcher<T> { protected Dispatcher(){} public static void DoSomething(T arg) {_impl.DoSomethingImpl(arg);} protected virtual void DoSomethingImpl(T arg) { } protected static readonly Dispatcher<T> _impl; static Dispatcher() { Type t = typeof (T); if (typeof(IFoo).IsAssignableFrom(t)) { Type implType = typeof (DispatcherIFoo<>).MakeGenericType(t); _impl = implType.GetConstructor(Type.EmptyTypes).Invoke(null) as Dispatcher<T>; } else { _impl = new Dispatcher<T>(); } } } internal class DispatcherIFoo<T> : Dispatcher<T> where T : IFoo { protected override void DoSomethingImpl(T arg) { arg.Foo(); } }
セカンダリ制約を追加した派生クラスの仮想関数を用いるこの方法は,System.Collections.Generic.Comparer<T> が用いているのと同じものです.個々の実装クラスを強い制約の下で実装しつつ,利用者にはそれを見せません.Comparer<T> クラスは,コード検証を行う 3 種類の比較実装クラスを,以下のそれぞれに応じて使い分けています.
- T が IComparable<T> を実装している場合
- T == Nullable<U> かつ U が IComparable<U> を実装している場合
- その他
同様の内容は,より簡潔に generic delegate で記述することもできます.
public static class Dispatcher<T> { private delegate void Func(T arg); public static void DoSomething(T arg) { _impl(arg); } private static readonly Func _impl; static Dispatcher() { Type t = typeof(T); if (typeof(IFoo).IsAssignableFrom(t)) { MethodInfo m = typeof(Dispatcher<T>).GetMethod( "CallFoo", BindingFlags.NonPublic | BindingFlags.Static); MethodInfo gm = m.MakeGenericMethod(t); _impl = Delegate.CreateDelegate(typeof(Func), gm) as Func; } else { _impl = NullMethod; } } private static void NullMethod(T arg) { } private static void CallFoo<U>(U arg) where U:IFoo{ arg.Foo(); } }
ここで紹介したジェネリクスによる boxing の回避とインターフェイス指向プログラミングの両立というテクニックは,約 2 年前の Visual C# 2005 のベータテスト期間中に,「BCL Team Blog」ですでに取り上げられているものです.
『Avoiding Boxing in Classes Implementing Generic Interfaces through Reflection』
さて,実行速度を考えればインターフェイス経由のメソッド呼び出しでインライン展開が行われないことによるビハインドが大きいため,boxing を無くしても本質的に速度の問題は解決しない,という向きもあるかと思いますが,この問題に関して個人的には Expression Tree に期待しています.
例えばクイックソートの Expression Tree と,比較関数の Expression Tree を実行時に合成,つまり手動で Expression Tree 上のインライン展開を行い,それを動的にコンパイルすることで,完全にインライン展開された JIT コードを手に入れられるものと考えられます.現在ネットワーク的に僻地なので今回例は示しませんが,Orcas CTP の 2 月版が出た頃に可能ならもう一度取り上げてみたいと思います.
*1:id:NyaRuRu:20060802:p1