Search code examples
c#dictionaryexpandoobject

why is ExpandoObject much slower than Dictionary?


I did two tests.

1- how long does it take to add 100k elements.

2- how many searches he can do in 10 seconds using 100k elements.

my results were these

ExpandoObject Add counter 97075

Dictionary Add counter 35

ExpandoObject Search counter 2396

Dictionary Search counter 1957637

conclusion:

to Add an ExpandoObject element was 2773 times slower.

to Search an ExpandoObject element was 817 times slower.

why is ExpandoObject much slower than 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);

        }
    }
}

Solution

  • The main difference is that Expando object does a linear search o(n) for each TryGetValue / TrySetValue.

    While a real Dictionary uses GetHashCode() and searches in a very small bucket for items.

    Thats particularly noticeable when you create big ExpandoObjects like in your test.

    This is the source code which both TryGetValue and TrySetValue of ExpandoObject use:

    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;
    }
    

    Or look at the code for 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);
    }
    

    In addition there is some reflection vodoo, locking and casting overhead for ExpandoObject