不使用反射進行C#屬性的執行時動態訪問

weixin_33912246發表於2012-02-11
  1. 摘要

  2. 問題的抽象

  3. 沒有優化的反射

  4. 使用晚繫結優化的反射

    • 公平的競賽
    • 公平的實現方式
  5. 換個思路,最直白的實現方式
  6. 執行時生成程式碼

  7. 效能比拼


摘要

單純的反射帶來靈活性的同時,也大大降低了應用程式的效率。本文將利用C#的各種技術,就如何實現動態的方法呼叫或屬性訪問做一些初步的研究。希望可以給同樣需要提高反射效能的朋友一些幫助。

問題的抽象

反射可以用在很多的情景中,但是抽象來看就是用來訪問編譯時無法確定的成員。這成員可以是方法,也可以是屬性。為了簡化問題,我們把問題限定在屬性的訪問上。那麼反射這個功能就可以抽象成下面這個介面。

/// <summary>
/// Abstraction of the function of accessing member of a object at runtime.
/// </summary>
public interface IMemberAccessor
{
    /// <summary>
    /// Get the member value of an object.
    /// </summary>
    /// <param name="instance">The object to get the member value from.</param>
    /// <param name="memberName">The member name, could be the name of a property of field. Must be public member.</param>
    /// <returns>The member value</returns>
    object GetValue(object instance, string memberName);

    /// <summary>
    /// Set the member value of an object.
    /// </summary>
    /// <param name="instance">The object to get the member value from.</param>
    /// <param name="memberName">The member name, could be the name of a property of field. Must be public member.</param>
    /// <param name="newValue">The new value of the property for the object instance.</param>
    void SetValue(object instance, string memberName, object newValue);
}

下面我們就來探討這個介面怎麼實現才能達到最高效率。

沒有優化的反射

使用反射是實現上面介面的最直觀最簡單的方式。程式碼如下:

public class ReflectionMemberAccessor : IMemberAccessor
{
    public object GetValue(object instance, string memberName)
    {
        var propertyInfo = instance.GetType().GetProperty(memberName);
        if (propertyInfo != null)
        {
            return propertyInfo.GetValue(instance, null);
        }

        return null;
    }

    public void SetValue(object instance, string memberName, object newValue)
    {
        var propertyInfo = instance.GetType().GetProperty(memberName);
        if (propertyInfo != null)
        {
            propertyInfo.SetValue(instance, newValue, null);
        }
    }
}

但是這種方式的效率讓人望而卻步。經過分析我們可以發現最慢的部分就是GetValue和SetValue這兩個呼叫。

使用Delegate優化的反射

將PropertyInfo的XetValue代理起來是最簡單的提高效能方法。而且也已經有很多人介紹了這種方式,

1. Fast Dynamic Property Field Accessors

2. 晚繫結場景下物件屬性賦值和取值可以不需要PropertyInfo

如果僅僅是看到他們的測試結果,會以為晚繫結就可以讓屬性的動態訪問的速度達到和直接取值一樣的速度,會覺得這生活多麼美好啊。但是如果你真的把這個技術用在個什麼地方會發現根本不是這麼回事兒。真實的生活會如老趙寫的Fast Reflection Library中給出的測試結果一般。你會發現即使是晚繫結了或是Emit了,速度也是要比直接訪問慢5-20倍。是老趙的實現方式有問題嗎?當然不是。

公平的競賽

這裡明確一下我們要實現的功能是什麼?我們要實現的功能是,用一組方法或是模式,動態地訪問任何一個物件上的任何一個屬性。而前面那些看些美好的測試,都只是在測試晚繫結後的委託呼叫的效能,而那測試用的晚繫結委託呼叫都是針對某個類的某個屬性的。這不是明擺著欺負反射嗎?雖然測試用的反射Invoke也是針對一個屬性,但是反射的通用版本的效能也是差不多的,Invoke才是消耗的大頭。這也是資料統計蒙人的最常見的手法,用自己最好的一部分和對方的最差的一部分去比較。但是我們真正關心的是整體。

用晚繫結這個特性去實現類似反射能實現的功能,是需要把每個類的每個屬性都快取起來,並且在使用的時候,根據當前物件的型別和所取的屬性名查詢對應的快取好的晚繫結委託。這些功能在那些美好的測試結果中都完全沒有體現出來。而老趙的Fast Reflection Libary實現了這些功能,所以測試結果看上去要差很多。但是這才是真實的資料。

公平的實現方式

為了文章的完整起見,Delegate反射的實現方式如下。(我這裡為了簡單起見,沒有過多優化,如果你要用這個方法,還是有很大的優化空間的。)

方法有兩種,一種是使用Delegate.CreateDelegate函式。一種是使用Expression Tree

使用Delegate的核心程式碼分別如下所示:




Delegate.CreateDelegate在使用上有一個要求,其生成的Delegate的簽名必須與Method的宣告一致。所以就有了上面使用泛型的方式。每個PropertyAccessor是針對特定屬性的,要真正用起來還要用Dictionary做下Mapping。如下所示:

public class DelegatedReflectionMemberAccessor : IMemberAccessor
{
    private static Dictionary<string, INamedMemberAccessor> accessorCache = new Dictionary<string, INamedMemberAccessor>();

    public object GetValue(object instance, string memberName)
    {
        return  FindAccessor(instance, memberName).GetValue(instance);
    }

    public void SetValue(object instance, string memberName, object newValue)
    {
        FindAccessor(instance, memberName).SetValue(instance, newValue);
    }

    private INamedMemberAccessor FindAccessor(object instance, string memberName)
    {
        var type = instance.GetType();
        var key = type.FullName + memberName;
        INamedMemberAccessor accessor;
        accessorCache.TryGetValue(key, out accessor);
        if (accessor == null)
        {
            var propertyInfo = type.GetProperty(memberName);
            accessor = Activator.CreateInstance(typeof(PropertyAccessor<,>).MakeGenericType(type, propertyInfo.PropertyType), type, memberName) as INamedMemberAccessor;
            accessorCache.Add(key, accessor);
        }

        return accessor;
    }
}

用ExpressionTree的生成委託的時候,也會遇到型別的問題,但是我們可以在ExpressionTree中對引數和返回值的型別進行處理,這樣就不需要泛型的實現方式了。程式碼如下:

public class DelegatedExpressionMemberAccessor : IMemberAccessor
{
    private Dictionary<string, Func<object, object>> getValueDelegates = new Dictionary<string, Func<object, object>>();
    private Dictionary<string, Action<object, object>> setValueDelegates = new Dictionary<string, Action<object, object>>();

    public object GetValue(object instance, string memberName)
    {
        var type = instance.GetType();
        var key = type.FullName + memberName;
        Func<object, object> getValueDelegate;
        getValueDelegates.TryGetValue(key, out getValueDelegate);
        if (getValueDelegate == null)
        {
            var info = type.GetProperty(memberName);
            var target = Expression.Parameter(typeof(object), "target");

            var getter = Expression.Lambda(typeof(Func<object, object>),
                Expression.Convert(Expression.Property(Expression.Convert(target, type), info), typeof(object)),
                target
                );

            getValueDelegate = (Func<object, object>)getter.Compile();
            getValueDelegates.Add(key, getValueDelegate);
        }

        return getValueDelegate(instance);
    }
}

一個優化方式是,把這個類做成泛型類,那麼key就可以只是memberName,這樣就少去了type.FullName及一次字串拼接操作。效能可以提高不少。但是這種委託式的訪問就是效能上的極限了嗎?如果是我就不用來寫這篇文章了。

雖然山寨卻更直接的方法

我們的目標是動態的訪問一個物件的一個屬性,一談到動態總是會自然而然地想到反射。其實還有一個比較質樸的方式。就是讓這個類自己去處理。還記得一開始定義的IMemberAccessor介面嗎?如果我們所有的類的都實現了這個介面,那麼就直接呼叫這個方法就是了。方式如下。

public class Man : IMemberAccessor
{
    public string Name { get; set; }

    public int Age { get; set; }

    public DateTime Birthday { get; set; }

    public double Weight { get; set; }

    public double Height { get; set; }

    public decimal Salary { get; set; }

    public bool Married { get; set; }

    public object GetValue(object instance, string memberName)
    {
        var man = instance as Man;
        if (man != null)
        {
            switch (memberName)
            {
                case "Name": return man.Name;
                case "Age": return man.Age;
                case "Birthday": return man.Birthday;
                case "Weight": return man.Weight;
                case "Height": return man.Height;
                case "Salary": return man.Salary;
                case "Married": return man.Married;
                default:
                    return null;
            }
        }
        else
            throw new InvalidProgramException();
    }

    public void SetValue(object instance, string memberName, object newValue)
    {
        var man = instance as Man;
        if (man != null)
        {
            switch (memberName)
            {
                case "Name": man.Name = newValue as string; break;
                case "Age": man.Age = Convert.ToInt32(newValue); break;
                case "Birthday": man.Birthday = Convert.ToDateTime(newValue); break;
                case "Weight": man.Weight = Convert.ToDouble(newValue); break;
                case "Height": man.Height = Convert.ToDouble(newValue); break;
                case "Salary": man.Salary = Convert.ToDecimal(newValue); break;
                case "Married": man.Married = Convert.ToBoolean(newValue); break;
            }
        }
        else
            throw new InvalidProgramException();
    }
}

有人可能會擔心用這種方式,屬性多了之後效能會下降。如果你用Reflector之類的工具反編譯一下生成的DLL,你就不會有這種顧慮了。C#對於 switch語句有相當力度的優化。簡略地講,當屬性少時會將switch生成為一堆if else。對於欄位型別為string,也會自動地轉成dictionary + int。

經過測試這種方式比上面的快取晚繫結的方式要快一倍。但是劣勢也很明顯,就是程式碼量太大了,而且不是一個好的設計,也不優雅。

用動態生成的工具函式讓動態屬性訪問更快一些

上面的方法速度上其實是最有優勢的,但是缺乏可操作性。但是如果我們能為每個類動態地生成兩個Get/Set方法,那麼這個方法就實際可用了。注意,這時的動態呼叫並不是反射呼叫了。生成的方式就是使用Expression Tree編譯出函式。

又因為這個方式是每個類一個函式,不像之前的方式都是一個屬性一個訪問物件。我們就可以利用C#的另一個特性來避免Dictionary的使用——泛型類中的靜態成員:如果GenericClass<T>中定義的靜態成員staticMember,那麼GenericClass<A>中的staticMember和GenericClass<B>中的staticMember是不共享的。雖然查詢泛型類也需要額外的執行時工作,但是代價比Dictionary查詢要低。

在這個方法中,既沒有用到反射,也沒有用到快取Dictionary。能更好地保證與手工程式碼效能的一致度。

實現的程式碼如下,鑑於程式碼量,只列出了get方法的程式碼:

public class DynamicMethod<T> : IMemberAccessor
{
    internal static Func<object, string, object> GetValueDelegate;

    public object GetValue(object instance, string memberName)
    {
        return GetValueDelegate(instance, memberName);
    }

    static DynamicMethod()
    {
        GetValueDelegate = GenerateGetValue();
    }

    private static Func<object, string, object> GenerateGetValue()
    {
        var type = typeof(T);
        var instance = Expression.Parameter(typeof(object), "instance");
        var memberName = Expression.Parameter(typeof(string), "memberName");
        var nameHash = Expression.Variable(typeof(int), "nameHash");
        var calHash = Expression.Assign(nameHash, Expression.Call(memberName, typeof(object).GetMethod("GetHashCode")));
        var cases = new List<SwitchCase>();
        foreach (var propertyInfo in type.GetProperties())
        {
            var property = Expression.Property(Expression.Convert(instance, typeof(T)), propertyInfo.Name);
            var propertyHash = Expression.Constant(propertyInfo.Name.GetHashCode(), typeof(int));

            cases.Add(Expression.SwitchCase(Expression.Convert(property, typeof(object)), propertyHash));
        }
        var switchEx = Expression.Switch(nameHash, Expression.Constant(null), cases.ToArray());
        var methodBody = Expression.Block(typeof(object), new[] { nameHash }, calHash, switchEx);

        return Expression.Lambda<Func<object, string, object>>(methodBody, instance, memberName).Compile();
    }
}

但是,好吧,問題來了。泛型類就意味著需要在寫程式碼的時候,或者說編譯時知道物件的型別。這樣也不符合我們一開始定義的目標。當然解決方案也是有的,就是再把那個Dictionary快取請回來。具體方式參考上面的給Delegate做快取的程式碼。

還有一個問題就是,這種Switch程式碼的效能會隨著Property數量的增長而呈現大致為線性的下降。會最終差於Delegate快取方式的呼叫。但是好在這個臨界點比較高,大致在40個到60個屬性左右。

效能測試

我們先把所有的方式列一下。

  1. 直接的物件屬性讀寫
  2. 單純的反射
  3. 使用Delegate.CreateDelegate生成委託並快取
  4. 使用Expression Tree生成屬性訪問委託並快取
  5. 讓物件自己實現IMemberAccessor介面,使用Switch Case。
  6. 為每個類生成IMemberAcessor介面所定義的函式。(非泛型方式呼叫)
  7. 為每個類生成IMemberAcessor介面所定義的函式。(泛型方式呼叫)

我們來看一下這6種實現對應的7種使用方式的效能。

Debug:執行1000萬次

方法 第一次結果 第二次結果
直接呼叫 208ms 227ms
反射呼叫 21376ms 21802ms
Expression委託 4341ms 4176ms
CreateDelegate委託 4204ms 4111ms
物件自身Switch 1653ms 1338ms
動態生成函式 2123ms 2051ms
(泛型)動態生成函式 1167ms 1157ms

Release:執行1000萬次

方法 第一次結果 第二次結果
直接呼叫 73ms 77ms
反射呼叫 20693ms 21229ms
Expression委託 3852ms 3853ms
CreateDelegate委託 3704ms 3748ms
物件自身Switch 1105ms 1116ms
動態生成函式 1678ms 1722ms
(泛型)動態生成函式 843ms 862ms

動態生成的函式比手寫的Switch還要快的原因是手寫的Switch要使用到Dictionary來將String型別的欄位名對映到int值。而我們生成的Switch是直接使用屬性的hashcode值進行的。所以會更快。完整的程式碼可以從這裡下載到。

相關文章