これでEvalの性能が悪いのを嫌う理由はないでしょう?

22743 ワード

Updated:思考問題の解答を提供する

Evalは性能が悪いと言います


ASPを書く.NETでEvalを使うのはごくありふれた手段で、どのASPでもあるようです.NET本では、DataTableをコントロールにバインドし、Evalで値を取る方法について説明しています.しかし、現在のDDD(Domain Driven Design)時代には、私たちが操作しているのは分野モデルオブジェクトであることが多い.IEnumerableを実装した任意のオブジェクトをバインドコントロールのデータソースとして使用し、バインドコントロールでEvalによってフィールドの値を取得できます.次のようになります.
protected void Page_Load(object sender, EventArgs e)
{
    List<Comment> comments = GetComments();
    this.rptComments.DataSource = comments;
    this.rptComments.DataBind();
}

<asp:Repeater runat="server" ID="rptComments">
    <ItemTemplate>
        Title: # Eval("Title") %><br />
        Conent: # Eval("Content") %>
    ItemTemplate>
    <SeparatorTemplate>
        <hr />
    SeparatorTemplate>
asp:Repeater>

ここで、Evealオブジェクトは反射によってTitleとContent属性の値を取得します.「反射、性能がどれほど悪いか、私は使わない!」という人がよくいます.ここで私はやはりこのような枝葉末節の性能を追求するやり方に保留的な態度を持っています.もちろん、上記の例では、書き方を変えることができます.
<asp:Repeater runat="server" ID="rptComments">
    <ItemTemplate>
        Title: # (Container.DataItem as Comment).Title %><br />
        Conent: # (Container.DataItem as Comment).Content %>
    ItemTemplate>
    <SeparatorTemplate>
        <hr />
    SeparatorTemplate>
asp:Repeater>

私たちはContainerを通じてDataItemは、現在の遍歴中のデータ・オブジェクトを取得し、Commentに変換してTitleとContentのプロパティを読み込みます.表現は少し長いですが、良い解決策のようです.性能はまぁ・・・向上したに違いない.
しかし、実際の開発では、特定のタイプのデータをデータソースとして簡単に使用できるとは限らず、2つのオブジェクトを組み合わせて共同表示する必要があることが多い.たとえば、コメントリストを表示するときに、投稿ユーザーの個人情報を表示することもよくあります.C#3.0では匿名オブジェクトがサポートされているため、次のことができます.
protected void Page_Load(object sender, EventArgs e)
{
    List<Comment> comments = GetComments();
    List<User> users = GetUsers();

    this.rptComments.DataSource = from c in comments
                                  from u in users
                                  where c.UserID == u.UserID
                                  order by c.CreateTime
                                  select new
                                  {
                                      Title = c.Title,
                                      Content = c.Content,
                                      NickName = u.NickName
                                  };
    this.rptComments.DataBind();
}

我々は,LINQカスケードCommentとUserデータセットにより,データソースとしての匿名オブジェクトの集合を容易に構築することができる(LINQのすばらしさを見たかどうか).上記の匿名オブジェクトには、Title、Content、NickNameのいくつかの共通属性が含まれます.したがって、ページではEvealを使用してデータを取得します.
しかし、私はほとんど肯定することができて、またある人が叫びます:“LINQは役に立たない!私达はLINQを使わない!Eval性能が悪い!私达はEvalを使わない!”では、私は彼らのために「最も堅実な」技術で再実現することを難しくしません.
private Dictionary<int, User> m_users;
protected User GetUser(int userId)
{
    return this.m_users[userId];
}

protected void Page_Load(object sender, EventArgs e)
{
    List<Comment> comments = GetComments();
    List<User> users = GetUsers();

    this.m_users = new Dictionary<int, User>();
    foreach (User u in users)
    {
        this.m_users[u.UserID] = u;
    }

    this.rptComments.DataSource = comments;
    this.rptComments.DataBind();
}

<asp:Repeater runat="server" ID="rptComments">
    <ItemTemplate>
        Title: # (Container.DataItem as Comment).Title %><br />
        Conent: # (Container.DataItem as Comment).Content %><br />
        NickName: # this.GetUser((Container.DataItem as Comment).UserID).NickName %>
    ItemTemplate>
    <SeparatorTemplate>
        <hr />
    SeparatorTemplate>
asp:Repeater>

兄弟たちは自分で判断しましょう.

反射性能が悪い?そんな理屈があるんだろうな・・・


反射速度が遅い?私はそれが比較的遅いことに同意します.
反射はCPUを占めるのが多いですか?彼が相対的に多いことに同意します.
だからEvealは使うべきではありませんか?私は同意しません.どうして子供を汚い水と一緒に倒すことができますか.反射アクセス属性のパフォーマンスの問題を解決すればいいのではないでしょうか.
性能が悪いのはEvalが反射を用いているためであり,このような問題を解決する従来の方法はEmitを用いることである.でもNET 3.5にはすでにLambda Expressionがあるが、Lambda Expressionを動的に構築した後、そのCompile法によって依頼例を得ることができ、Emit実装の様々な詳細についてはすでに説明する.NETフレームワークが実現しました.これはあまり難しくありません.
public class DynamicPropertyAccessor
{
    private Func<object, object> m_getter;

    public DynamicPropertyAccessor(Type type, string propertyName)
        : this(type.GetProperty(propertyName))
    { }

    public DynamicPropertyAccessor(PropertyInfo propertyInfo)
    {
        // target: (object)((({TargetType})instance).{Property})

        // preparing parameter, object type
        ParameterExpression instance = Expression.Parameter(
            typeof(object), "instance");

        // ({TargetType})instance
        Expression instanceCast = Expression.Convert(
            instance, propertyInfo.ReflectedType);

        // (({TargetType})instance).{Property}
        Expression propertyAccess = Expression.Property(
            instanceCast, propertyInfo);

        // (object)((({TargetType})instance).{Property})
        UnaryExpression castPropertyValue = Expression.Convert(
            propertyAccess, typeof(object));

        // Lambda expression
        Expression<Func<object, object>> lambda = 
            Expression.Lambda<Func<object, object>>(
                castPropertyValue, instance);

        this.m_getter = lambda.Compile();
    }

    public object GetValue(object o)
    {
        return this.m_getter(o);
    }
}

DynamicPropertyAccessorでは、特定の属性に対してo=>object((Class)oという形を構築する.PropertyのLambda式は、CompileによってFuncに委任されます.最終的にGetValueメソッドにClassタイプのオブジェクトを入力することで、その指定したプロパティの値を取得できます.
この方法は見覚えがあるのではないでしょうか.そう、私は『メソッドの直接呼び出し、反射呼び出しと......Lambda式呼び出し』でも似たようなやり方を使っています.

性能をテストしますか?


属性の直接取得値,反射取得値,Lambda式取得値の3つの方法の性能を比較する.
var t = new Temp { Value = null };

PropertyInfo propertyInfo = t.GetType().GetProperty("Value");
Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (var i = 0; i < 1000000; i ++)
{
    var value = propertyInfo.GetValue(t, null);
}
watch1.Stop();
Console.WriteLine("Reflection: " + watch1.Elapsed);

DynamicPropertyAccessor property = new DynamicPropertyAccessor(t.GetType(), "Value");
Stopwatch watch2 = new Stopwatch();
watch2.Start();
for (var i = 0; i < 1000000; i++)
{
    var value = property.GetValue(t);
}
watch2.Stop();
Console.WriteLine("Lambda: " + watch2.Elapsed);

Stopwatch watch3 = new Stopwatch();
watch3.Start();
for (var i = 0; i < 1000000; i++)
{
    var value = t.Value;
}
watch3.Stop();
Console.WriteLine("Direct: " + watch3.Elapsed);

結果は次のとおりです.
Reflection: 00:00:04.2695397
Lambda: 00:00:00.0445277
Direct: 00:00:00.0175414

DynamicPropertyAccessorを使用すると、ダイレクトコールよりもパフォーマンスがやや遅くなり、百倍の差があります.さらに、DynamicPropertyAccessorは匿名オブジェクトのプロパティの値を取得することもサポートしています.これは,我々のEval法がDynamicPropertyAccessorに完全に依存できることを意味する.

高速エバーから一歩しか離れていない


「一歩の距離」?そう、それはキャッシュです.DynamicPropertyAccessorを呼び出すGetValueメソッドは時間がかかりますが、DynamicPropertyAccessorオブジェクトを構築するには時間がかかります.したがって、次のようにDynamicPropertyAccessorオブジェクトをキャッシュする必要があります.
public class DynamicPropertyAccessorCache
{
    private object m_mutex = new object();
    private Dictionary<Type, Dictionary<string, DynamicPropertyAccessor>> m_cache =
        new Dictionary<Type, Dictionary<string, DynamicPropertyAccessor>>();

    public DynamicPropertyAccessor GetAccessor(Type type, string propertyName)
    {
        DynamicPropertyAccessor accessor;
        Dictionary<string, DynamicPropertyAccessor> typeCache;

        if (this.m_cache.TryGetValue(type, out typeCache))
        {
            if (typeCache.TryGetValue(propertyName, out accessor))
            {
                return accessor;
            }
        }

        lock (m_mutex)
        {
            if (!this.m_cache.ContainsKey(type))
            {
                this.m_cache[type] = new Dictionary<string, DynamicPropertyAccessor>();
            }

            accessor = new DynamicPropertyAccessor(type, propertyName);
            this.m_cache[type][propertyName] = accessor;

            return accessor;
        }
    }
}

テストの結果、DynamicPropertyAccessorオブジェクトをキャッシュから取得するたびに、呼び出しのパフォーマンスが低下しますが、反射呼び出しよりも数十倍も速くなります.

FastEveal--断る人はいますか?


FastEvealメソッドは、前の.NETバージョンでは、各ページの共通ベースクラスに定義できます.でも私たちが使っている以上.NET 3.5は、Extension Methodという侵入のない方法で実現できます.
public static class FastEvalExtensions
{
    private static DynamicPropertyAccessorCache s_cache = 
        new DynamicPropertyAccessorCache();

    public static object FastEval(this Control control, object o, string propertyName)
    {
        return s_cache.GetAccessor(o.GetType(), propertyName).GetValue(o);
    }

    public static object FastEval(this TemplateControl control, string propertyName)
    {
        return control.FastEval(control.Page.GetDataItem(), propertyName);
    }
}

Controlでの拡張により、各ページで1つのオブジェクトと属性名で直接値を取得できるようになりました.一方、TemplateControlでの拡張により、各種バインド可能なコントロールやページ(Page,MasterPage,UserControl)は、現在バインドされているデータオブジェクトの属性値を属性名で直接取得することができる.
今、FastEvealを拒否する理由は何ですか?

その他


実は私たちの文章全体がEvalの方法の役割を軽視しています.Evalメソッドの文字列パラメータ名は「expression」、すなわち式です.実際には「.」も使えますなどのフォントスタイルや効果を適用します.では、私たちのFastEvalはこれをすることができますか?もちろんいいです.ただ、これはあなた自身が実現する必要があります.:)
最後に、DynamicPropertyAccessorではGetValueメソッドが1つしか提供されていませんが、このプロパティを設定するためにSetValueメソッドを追加できますか?皆さんが積極的に返事をしてほしいです.後で私のやり方を提供します.

思考問題の解答


一つのプロパティは、実際にはget/setメソッドのペアで構成されていることを知っておく必要があります(もちろん、そのうちの1つが欠けている可能性があります).プロパティのPropertyInfoオブジェクトを取得すると、そのGetSetMethodメソッドで設定メソッドを取得できます.次の仕事は、「メソッドの直接呼び出し、反射呼び出しと......Lambda式呼び出し」の文のDynamicMethodExecutorに完全に任せることができるのではないでしょうか.したがって、DynamicPropertyAccessorにSetValueメソッドを追加するのも簡単です.
public class DynamicPropertyAccessor
{
    ...
    private DynamicMethodExecutor m_dynamicSetter;

    ...

    public DynamicPropertyAccessor(PropertyInfo propertyInfo)
    {
        ...

        MethodInfo setMethod = propertyInfo.GetSetMethod();
        if (setMethod != null)
        {
            this.m_dynamicSetter = new DynamicMethodExecutor(setMethod);
        }
    }

    ...

    public void SetValue(object o, object value)
    {
        if (this.m_dynamicSetter == null)
        {
            throw new NotSupportedException("Cannot set the property.");
        }

        this.m_dynamicSetter.Execute(o, new object[] { value });
    }
}

以下のコメントでは、Such Cloudは似たようなやり方を考えており、励ましに値し、サポートに感謝しています.