为什么ExpandoObject比Dictionary慢?

3

我进行了两个测试。

1- 添加10万个元素需要多长时间。

2- 在10秒钟内使用10万个元素可以进行多少次搜索。

我的测试结果如下:

ExpandoObject添加计数器为97075。

Dictionary添加计数器为35。

ExpandoObject搜索计数器为2396。

Dictionary搜索计数器为1957637。

结论:

添加ExpandoObject元素比添加Dictionary元素慢2773倍。

搜索ExpandoObject元素比搜索Dictionary元素慢817倍。

为什么ExpandoObject比Dictionary慢得多?

using System;
using System.Dynamic;
using System.Collections.Generic;
using System.Threading;

namespace c_sharp_benchmark

{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkExpandoObjectAdd();
            BenchmarkDictionaryAdd();
            BenchmarkExpandoObjectSearch();
            BenchmarkDictionarySearch();
        }
        static void BenchmarkExpandoObjectAdd()
        {
            dynamic exp = new ExpandoObject();
            var expid = (IDictionary<string, object>)exp;
            Random rnd = new Random();
            long old = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            int i;
            for (i = 0; i < 100000; i++)
            {
                expid.Add("Prop" + i, i);
            }
            Console.WriteLine("ExpandoObject Add counter " + (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - old));
        }
        static void BenchmarkDictionaryAdd()
        {
            Dictionary<string, object> dic = new Dictionary<string, object>();
            Random rnd = new Random();


            long old = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            int i;
            for (i = 0; i < 100000; i++)
            {
                dic.Add("Prop" + i, i);
            }
            Console.WriteLine("Dictionary Add counter " + (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - old));
        }

        static void BenchmarkExpandoObjectSearch()
        {
            dynamic exp = new ExpandoObject();
            var expid = (IDictionary<string, object>)exp;
            Random rnd = new Random();
            int i;
            for (i = 0; i < 100000; i++)
            {
                expid.Add("Prop" + i, i);
            }
            int auxval;
            long when_stop = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 10000;
            int counter = 0;
            while (when_stop > DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
            {
                ++counter;
                auxval = (int)expid["Prop" + rnd.Next(100000)];
            }
            Console.WriteLine("ExpandoObject Search counter " + counter / 10);
        }
        static void BenchmarkDictionarySearch()
        {
            Dictionary<string, object> dic = new Dictionary<string, object>();
            Random rnd = new Random();
            int i;
            for (i = 0; i < 100000; i++)
            {
                dic.Add("Prop" + i, i);
            }
            int auxval;
            long when_stop = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 10000;
            int counter = 0;
            while (when_stop > DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
            {
                ++counter;
                auxval = (int)dic["Prop" + rnd.Next(100000)];
            }
            Console.WriteLine("Dictionary Search counter " + counter / 10);

        }
    }
}

1
应该使用Stopwatch来进行精确和准确的性能计时,而不是使用DateTimeOffset.UtcNow。请参阅性能测试的精确时间测量 - dbc
1个回答

7
主要区别在于Expando对象对于每个TryGetValue/TrySetValue都执行线性搜索o(n)。而真正的字典使用GetHashCode()并在非常小的桶中搜索项目。当你创建像测试中那样的大型ExpandoObjects时,这尤其明显。以下是ExpandoObject的TryGetValue和TrySetValue使用的源代码:
internal int GetValueIndexCaseSensitive(string name)
{
    for (int i = 0; i < _keys.Length; i++)
    {
        if (string.Equals(_keys[i], name, StringComparison.Ordinal))
        {
            return i;
        }
    }
    return -1;
}

或者查看BindGetOrInvokeMember代码。
private DynamicMetaObject BindGetOrInvokeMember(DynamicMetaObjectBinder binder, string name, bool ignoreCase, DynamicMetaObject fallback, Func<DynamicMetaObject, DynamicMetaObject> fallbackInvoke)
{
    ExpandoClass @class = Value.Class;
    int valueIndex = @class.GetValueIndex(name, ignoreCase, Value);
    ParameterExpression parameterExpression = Expression.Parameter(typeof(object), "value");
    Expression test = Expression.Call(typeof(RuntimeOps).GetMethod("ExpandoTryGetValue"), GetLimitedSelf(), Expression.Constant(@class, typeof(object)), Expression.Constant(valueIndex), Expression.Constant(name), Expression.Constant(ignoreCase), parameterExpression);
    DynamicMetaObject dynamicMetaObject = new DynamicMetaObject(parameterExpression, BindingRestrictions.Empty);
    if (fallbackInvoke != null)
    {
        dynamicMetaObject = fallbackInvoke(dynamicMetaObject);
    }
    dynamicMetaObject = new DynamicMetaObject(Expression.Block(new ParameterExpression[1]
    {
        parameterExpression
    }, Expression.Condition(test, dynamicMetaObject.Expression, fallback.Expression, typeof(object))), dynamicMetaObject.Restrictions.Merge(fallback.Restrictions));
    return AddDynamicTestAndDefer(binder, Value.Class, null, dynamicMetaObject);
}

此外,使用ExpandoObject还存在反射、锁定和类型转换方面的开销。


请确认,ExpandoObject的源代码在此处,内部的ExpandoClass源代码在此处 - dbc
这很棒,谢谢你的出色回答。不幸的是,我无法点赞,因为我没有声望。 - cronos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接