パフォーマンスのための Delegate, LCG, LINQ, DLR (を後で書く)

『KazzzのJとNのはざまで』より.
コメントで書こうかと思ったのですが,長いのでこちらにて.

以下はdotTraceにより、AOP機能で実行されているINotifyPropertyChanged#NotifyPropertyChangedメソッドのプロファイル結果からホットスポットと思われるサブツリーを抽出したダンプ結果だが、テストでは500件×4カラムのデータをDatGridViewにバインド中のDTOに追加している際に2000回のNotifyPropertyChangedメソッドーのコールが発生し、8秒程度の時間を消費しているのが解る。(プロファイルしながらの実行なので実際の処理に8秒かかる訳ではない)

98.35% Invoke - 8238.7 ms - 2000 calls - Framework.DTO.NotifyPropertyChangedInterceptor.Invoke(IMethodInvocation)
  97.22% NotifyPropertyChanged - 8144.5 ms - 2000 calls - Framework.DTO.Impl.DTOImpl.NotifyPropertyChanged(String)
    97.12% System.ComponentModel.BindingList<T>.Child_PropertyChanged... - 8135.7 ms - 2000 calls
      53.62% Invoke - 4491.7 ms - 127249 calls - Framework.Aop.AopProxy.Invoke(IMessage)
        21.50% Invoke - 1800.7 ms - 127249 calls - Framework.Util.MethodUtil.Invoke(MethodBase, Object, Object )
          21.18% Invoke - 1774.1 ms - 127249 calls - System.Reflection.MethodBase.Invoke(Object, Object )
        18.55% get_MethodBase - 1553.6 ms - 127249 calls - System.Runtime.Remoting.Messaging.Message.get_MethodBase()
        4.80% get_Args - 402.3 ms - 127249 calls - System.Runtime.Remoting.Messaging.Message.get_Args()
        4.71% ReturnMessage..ctor - 394.4 ms - 127249 calls - System.Runtime.Remoting.Messaging.ReturnMessage..ctor(Object, Object , Int32, LogicalCallContext, IMethodCallMessage)
        1.46% ContainsKey - 122.0 ms - 127249 calls - System.Collections.Generic.Dictionary<TKey, TValue>.ContainsKey(TKey)
        0.10% get_LogicalCallContext - 8.0 ms - 127249 calls - System.Runtime.Remoting.Messaging.Message.get_LogicalCallContext()
        0.10% get_Name - 8.0 ms - 127249 calls - System.Reflection.RuntimeMethodInfo.get_Name()
  0.39% System.Reflection.RuntimePropertyInfo.GetValue... - 32.4 ms - 4000 calls
  0.21% Proceed - 18.0 ms - 2000 calls - Framework.Aop.Impl.MethodInvocationImpl.Proceed()
  0.19% GetProperty - 16.0 ms - 2000 calls - System.Type.GetProperty(String, BindingFlags)
  0.13% StartsWith - 10.8 ms - 2000 calls - System.String.StartsWith(String)
  0.09% Split - 7.9 ms - 2000 calls - System.String.Split(String , StringSplitOptions)

System.Reflection.MethodBase.Invoke が重いのはデリゲートで回避できそうな気がするんですがどうでしょうね?
ある“動的”な処理が何度も繰り返されるとき,Type インスタンスや MethodInfo インスタンスなど,ごく少数の要素をキーとして固定すれば,残りは“静的な IL で表現できる”ことが多いので,だったらそれをデリゲートとしてキャッシュしましょうと.LINQ や DLR の実装を追いかけている人にはおなじみのテクニックですな.
コードはこんな感じで.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;

namespace NotifySetterTest
{
    public delegate void PropertySetter<TReciever, TProperty>(TReciever reciever, TProperty value)
        where TReciever : IEntity
        where TProperty : IEquatable<TProperty>;

    public static class Util
    {
        //下の LCG 実装の(擬似)等価コード
        //記法は,死に設定 "Extreme Late Binding" から Inspired!
        //http://www.atmarkit.co.jp/fdotnet/special/pdc2005_02/pdc2005_02_04.html
        //public static PropertySetter<TReciever, TProperty> CreateNotifySetter<TReciever, TProperty>(PropertyInfo prop)
        //    where TReciever : IEntity
        //    where TProperty : IEquatable<TProperty>
        //{
        //    return delegate( TReciever reciever, TProperty value )
        //    {
        //        TProperty preValue = reciever.(prop);
        //        reciever.(prop) = value;
        //        TProperty postValue = reciever.(prop);
        //
        //        if (!EqualityComparer<TProperty>.Default.Equals(preValue, postValue))
        //        {
        //            reciever.NotifyPropertyChanged(prop.Name);
        //        }
        //    };
        //}
        public static PropertySetter<TReciever, TProperty> CreateNotifySetter<TReciever, TProperty>(PropertyInfo propInfo)
            where TReciever : IEntity
            where TProperty : IEquatable<TProperty>
        {
            Type recvtype = typeof(TReciever);
            Type proptype = typeof(TProperty);

            Debug.Assert(propInfo.DeclaringType.IsAssignableFrom(recvtype));
            Debug.Assert(propInfo.PropertyType.IsAssignableFrom(proptype));

            DynamicMethod dm = new DynamicMethod(
                "set_" + propInfo.Name + "_and_notify",
                typeof(void),
                new Type[] { recvtype, proptype },
                propInfo.DeclaringType,
                true // false にすると MethodAccessException が発生
                );
            ILGenerator ilgen = dm.GetILGenerator();
            LocalBuilder lb_preValue = ilgen.DeclareLocal(proptype);
            LocalBuilder lb_postValue = ilgen.DeclareLocal(proptype);
            Label label_ret = ilgen.DefineLabel();
            const int arg_reciever = 0;
            const int arg_value = 1;

            ilgen.Emit(OpCodes.Ldarga_S, arg_reciever);
            ilgen.Emit(OpCodes.Constrained, recvtype);
            ilgen.EmitCall(OpCodes.Callvirt, propInfo.GetGetMethod(), null);
            ilgen.Emit(OpCodes.Stloc, lb_preValue);

            ilgen.Emit(OpCodes.Ldarga_S, arg_reciever);
            ilgen.Emit(OpCodes.Ldarg_1); // 1 = arg_value
            ilgen.Emit(OpCodes.Constrained, recvtype);
            ilgen.EmitCall(OpCodes.Callvirt, propInfo.GetSetMethod(), null);

            ilgen.Emit(OpCodes.Ldarga_S, arg_reciever);
            ilgen.Emit(OpCodes.Constrained, recvtype);
            ilgen.EmitCall(OpCodes.Callvirt, propInfo.GetGetMethod(), null);
            ilgen.Emit(OpCodes.Stloc, lb_postValue);

            PropertyInfo get_Comparer = typeof(EqualityComparer<TProperty>).GetProperty("Default", BindingFlags.Public | BindingFlags.Static);

            MethodInfo method_Equals = typeof(EqualityComparer<TProperty>).GetMethod("Equals", new Type[] { proptype, proptype });

            ilgen.EmitCall(OpCodes.Call, get_Comparer.GetGetMethod(), null);
            ilgen.Emit(OpCodes.Ldloc, lb_preValue);
            ilgen.Emit(OpCodes.Ldloc, lb_postValue);
            ilgen.EmitCall(OpCodes.Callvirt, method_Equals, null);

            ilgen.Emit(OpCodes.Brtrue_S, label_ret);

            MethodInfo method_Notify = typeof(IEntity).GetMethod("NotifyPropertyChanged", new Type[] { typeof(string) });

            ilgen.Emit(OpCodes.Ldarga_S, arg_reciever);
            ilgen.Emit(OpCodes.Ldstr, propInfo.Name);
            ilgen.Emit(OpCodes.Constrained, recvtype);
            ilgen.EmitCall(OpCodes.Callvirt, method_Notify, null);

            ilgen.MarkLabel(label_ret);
            ilgen.Emit(OpCodes.Ret);

            return dm.CreateDelegate(typeof(PropertySetter<TReciever, TProperty>))
                as PropertySetter<TReciever, TProperty>;
        }
    }

    public interface IEntity : INotifyPropertyChanged
    {
        void NotifyPropertyChanged(String propertyName);
    }

    public class Person : IEntity
    {
        private string lastName;
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        public virtual string LastName
        {
            get { return this.lastName; }
            set { this.lastName = value; }
        }
    }

    static class Program
    {
        static void Main(string[] args)
        {
            PropertyInfo propInfo = typeof(Person).GetProperty("LastName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);

            // この LastName_setter は TReciever + PropertyInfo をキーとしてキャッシュできる
            PropertySetter<Person, string> LastName_setter = Util.CreateNotifySetter<Person, string>(propInfo);

            Person target = new Person();
            target.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e) { Console.WriteLine(e.PropertyName + " Changed!"); };

            LastName_setter(target, "Taro");
        }
    }
}

月末までやばいぐらい時間がないのでとりあえずコードのみにて失礼.そのうち意義というかその辺を書きます.



http://blogs.wankuma.com/naka/archive/2007/09/24/97705.aspx
これもある意味同じ話題なのかも.

追記

菊池さんからご指摘があったので,擬似コードを少しいじって動くようにしておきました.
C# 2.0 風コードというと,こんな感じかと思います.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

namespace NotifySetterTest
{
    public delegate void PropertySetter<TReciever, TProperty>(TReciever reciever, TProperty value)
        where TReciever : IEntity
        where TProperty : IEquatable<TProperty>;
    public delegate TProperty PropertyGetter<TReciever, TProperty>(TReciever reciever)
        where TReciever : IEntity
        where TProperty : IEquatable<TProperty>;

    public static class Util
    {
        public static PropertySetter<TReciever, TProperty> CreateNotifySetter<TReciever, TProperty>(PropertyInfo prop)
            where TReciever : IEntity
            where TProperty : IEquatable<TProperty>
        {
            // (仮想) getter と setter の delegate 化
            // 相手が仮想メソッドでも何ら問題なく,
            // callvirt のセマンティクスを保ったまま (ここ重要),
            // 暗黙の this 引数を引きずり出せます
            PropertySetter<TReciever, TProperty> setter =
                Delegate.CreateDelegate(typeof(PropertySetter<TReciever, TProperty>), prop.GetSetMethod())
                as PropertySetter<TReciever, TProperty>;
            PropertyGetter<TReciever, TProperty> getter =
                Delegate.CreateDelegate(typeof(PropertyGetter<TReciever, TProperty>), prop.GetGetMethod())
                as PropertyGetter<TReciever, TProperty>;

            // ここで新しく変数を作っておくと fat な prop の参照をキャプチャしなくてすみます
            string propname = prop.Name;

            return delegate( TReciever reciever, TProperty value )
            {
                TProperty preValue = getter(reciever);
                setter(reciever, value);
                TProperty postValue = getter(reciever);
        
                if (!EqualityComparer<TProperty>.Default.Equals(preValue, postValue))
                {
                    reciever.NotifyPropertyChanged(propname);
                }
            };
        }
    }

    public interface IEntity : INotifyPropertyChanged
    {
        void NotifyPropertyChanged(String propertyName);
    }

    public class Person : IEntity
    {
        private string lastName;
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        public virtual string LastName
        {
            get { return this.lastName; }
            set { this.lastName = value; }
        }
    }

    static class Program
    {
        static void Main(string[] args)
        {
            PropertyInfo propInfo = typeof(Person).GetProperty("LastName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);

            // この LastName_setter は TReciever + PropertyInfo をキーとしてキャッシュできる
            PropertySetter<Person, string> LastName_setter = Util.CreateNotifySetter<Person, string>(propInfo);

            Person target = new Person();
            target.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e) { Console.WriteLine(e.PropertyName + " Changed!"); };

            LastName_setter(target, "Taro");
        }
    }
}