天天看點

使用表達式樹和反射來通路對象屬性的性能比較

今天在工作上遇到這麼個需求:需要擷取對象上所有屬性的值,但并事先并不知道對象的類型。 我的第一反應就是使用反射,但是這個操作會進行多次,大量的反射肯定會有性能影響。雖然對我這個項目無關緊要,但我還是選擇了另外一種解決方案:建構表達式樹,再生成委托,然後将委托緩存在字典裡。代碼如下:

首先建構表達式樹(類似這種形式:'(a) => a.xx'),并生成委托:

private static Func<TObject, object> BuildDynamicGetPropertyValueDelegate<TObject>(PropertyInfo property)
{
    var instanceExpression = Expression.Parameter(property.ReflectedType, "instance");
    var memberExpression = Expression.Property(instanceExpression, property);
    var convertExpression = Expression.Convert(memberExpression, typeof(object));
    var lambdaExpression = Expression.Lambda<Func<TObject, object>>(convertExpression, instanceExpression);
    return lambdaExpression.Compile();
}      

接着,當需要擷取屬性的值時,先在字典裡檢視是否有已經生成好的委托,有的話取出委托執行擷取屬性值。沒有則建構表達式樹生成委托,并放入字典中:

private static Dictionary<PropertyInfo, Delegate> delegateCache = new Dictionary<PropertyInfo, Delegate>();

public static object GetPropertyValueUseExpression<TObject>(TObject obj, PropertyInfo property)
{
    Delegate d;
    if (delegateCache.TryGetValue(property, out d))
    {
        var func = (Func<TObject, object>)d;
        return func(obj);
    }

    var getValueDelegate = BuildDynamicGetPropertyValueDelegate<TObject>(property);
    delegateCache[property] = getValueDelegate;
    return getValueDelegate(obj);
}      

就這麼簡單,完成之後,我想測試一下表達式樹版本和反射版本的性能差距如何,于是我又簡單實作反射版本作為測試對比:

public static object GetPropertyValueUseReflection<TObject>(TObject obj, PropertyInfo propertyInfo)
{
    return propertyInfo.GetValue(obj);
}      

接下來是兩者的測試代碼:

class Car 
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Capacity { get; set; }
}

.....

int repeatTimes = 10000;
PropertyInfo property = typeof(Car).GetProperty("Make");
Car car = new Car();

Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < repeatTimes; i++)
{
    GetPropertyValueUseExpression(car, property);
}
stopwatch.Stop();
Console.WriteLine("Repeated {0}, Cache in Dictionary expression used time: {1} ms", repeatTimes, stopwatch.ElapsedTicks);

stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < repeatTimes; i++)
{
    GetPropertyValueUseReflection(car, property);
}
stopwatch.Stop();
Console.WriteLine("Repeated {0}, reflection used time: {1} ms", repeatTimes, stopwatch.ElapsedTicks);      

在我的預想之中是這樣的:表達式樹版本在調用次數很少的情況下會慢于反射版本,随着次數增多,表達式樹版本的優勢會越來越明顯。

測試結果:

使用表達式樹和反射來通路對象屬性的性能比較
使用表達式樹和反射來通路對象屬性的性能比較
使用表達式樹和反射來通路對象屬性的性能比較

在調用次數為十萬、百萬、千萬次的情況下,表達式書版本的優勢随着次數而提高。

PS:之前在代碼中犯了一些錯誤導緻表達式樹版本的效率比反射還低,還把原因歸結于Dictionary的效率,确實不該。但是為何這些小錯誤會導緻如此的差距我還沒弄明白,搞明白之後再寫一篇部落格吧。

更新:

經過Echofool、zhaxg兩位園友的提示,其實通路屬性的委托可以不用放在字典裡,而是通過多接收一個參數再根據switch case來擷取相應的屬性值,代碼如下:

public class PropertyDynamicGetter<T>
{
    private static Func<T, string, object> cachedGetDelegate;

    public PropertyDynamicGetter()
    {
        if (cachedGetDelegate == null)
        {
            var properties = typeof(T).GetProperties();
            cachedGetDelegate = BuildDynamicGetDelegate(properties);
        }
    }

    public object Execute(T obj, string propertyName)
    {
        return cachedGetDelegate(obj, propertyName);
    }

    private Func<T, string, object> BuildDynamicGetDelegate(PropertyInfo[] properties)
    {
        var objParamExpression = Expression.Parameter(typeof(T), "obj");
        var nameParamExpression = Expression.Parameter(typeof(string), "name");
        var variableExpression = Expression.Variable(typeof(object), "propertyValue");

        List<SwitchCase> switchCases = new List<SwitchCase>();
        foreach (var property in properties)
        {
            var getPropertyExpression = Expression.Property(objParamExpression, property);
            var convertPropertyExpression = Expression.Convert(getPropertyExpression, typeof(object));
            var assignExpression = Expression.Assign(variableExpression, convertPropertyExpression);
            var switchCase = Expression.SwitchCase(assignExpression, Expression.Constant(property.Name));
            switchCases.Add(switchCase);
        }

        //set null when default
        var defaultBodyExpression = Expression.Assign(variableExpression, Expression.Constant(null));
        var switchExpression = Expression.Switch(nameParamExpression, defaultBodyExpression, switchCases.ToArray());
        var blockExpression = Expression.Block(typeof(object), new[] { variableExpression }, switchExpression);
        var lambdaExpression = Expression.Lambda<Func<T, string, object>>(blockExpression, objParamExpression, nameParamExpression);
        return lambdaExpression.Compile();
    }
}      

這個版本不使用字典,進而去除了從字典取對象的影響。它實作上先是取出對象所有的屬性,然後在建構表達式樹時根據屬性名使用Switch。

使用表達式樹和反射來通路對象屬性的性能比較

可以看到,在千萬次的情況下(十萬,百萬也是如此),這個版本效率比表達式樹緩存在字典裡的效率還要高一些。

最後,如果我的代碼有錯誤或者測試方法不對,歡迎大家指出