Производительность DynamicObject и TrySetMember против производительности ExpandoObject

Я использую пользовательскую реализацию DynamicObject, которая отлично работает для моего приложения, за исключением того факта, что я сталкиваюсь с некоторыми проблемами с производительностью. Можно ожидать некоторого снижения производительности с динамикой, но я вижу значительные (читай: порядки) потери в производительности даже по сравнению с использованием ExpandoObject.

Причина, по которой я не могу использовать ExpandoObject, заключается в том, что я хочу переопределить некоторые его свойства. Я свел проблему к очень простому примеру ниже.

Мой пользовательский код ExpandoObject выглядит следующим образом (упрощен до достаточно кода, чтобы продемонстрировать проблему) -

public class SuperExpando : DynamicObject
{
    public Dictionary<string, object> dictionary = new Dictionary<string, object>();
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        dictionary[binder.Name] = value;
        return true;
    }
}

public dynamic m = new SuperExpando();

Когда я устанавливаю значения в словаре DynamicObject напрямую (то есть, mdictionary["keyname"] = 500), тогда я вижу производительность, аналогичную производительности ExpandoObject, которая составляет менее миллисекунды для установки значения ключа в словаре. Когда я использую переопределение TrySetMember (iemkeyname = 500), я вижу снижение производительности до 30 - 50 мс на набор значений ключа. При записи на большое количество ключей это, очевидно, становится проблемой. Даже если я пишу один и тот же ключ снова и снова, доступ к нему через TrySetMember занимает одинаковое количество времени.

Моя реальная проблема с производительностью, похоже, не связана с тем, что я использую динамику, поскольку она выполняет переопределение TrySetMember. За пинки я даже закомментировал

dictionary[binder.Name] = value;

в методе TrySetMember и ничего не оставил, кроме как "вернуть истину;", и производительность была одинаковой.

Если я добавлю что-то вроде следующего в мой класс SuperExpando -

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (dictionary.ContainsKey(binder.Name))
    {
        result = dictionary[binder.Name];
        return true;
    }
    return false; 
}

Проблемы с производительностью доступа (чтения) к переменным через TryGetMember одинаковы, тогда как чтение словаря напрямую предлагает разумную производительность.

Есть идеи?

-BJ Куинн

РЕДАКТИРОВАТЬ: Вот полный пример кода. Просто создайте форму и поместите на нее кнопку, которая запускает событие go_Click и установите ваш проект как консольное приложение. Для меня требуется ~30 мсек, чтобы установить все 50 ключей в ExpandoObject, тогда как SuperExpando занимает минимум ~750 мс.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Dynamic;

namespace test
{
    public partial class ExpandoTest : Form
    {
        public ExpandoTest()
        {
            InitializeComponent();
        }

        public class SuperExpando : DynamicObject
        {
            public Dictionary<string, object> dictionary = new Dictionary<string, object>();

            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                //dictionary[binder.Name] = value;
                return true;
            }
        }

        DateTime lasttime = DateTime.Now;

        public void outputtime(string label = "")
        {
            TimeSpan elapsedtime = DateTime.Now - lasttime;
            Double elapsedms = elapsedtime.TotalMilliseconds;
            Console.WriteLine(label + " : " + elapsedms.ToString());
            lasttime = DateTime.Now;
        }

        private void go_Click(object sender, EventArgs e)
        {
            outputtime("Time spent waiting on user");
            dynamic se = new SuperExpando();
            outputtime("Declared SuperExpando");
            se.test120 = 5;
            se.test121 = 5;
            se.test122 = 5;
            se.test123 = 5;
            se.test124 = 5;
            se.test125 = 5;
            se.test126 = 5;
            se.test127 = 5;
            se.test128 = 5;
            se.test129 = 5;
            se.test130 = 5;
            se.test131 = 5;
            se.test132 = 5;
            se.test133 = 5;
            se.test134 = 5;
            se.test135 = 5;
            se.test136 = 5;
            se.test137 = 5;
            se.test138 = 5;
            se.test139 = 5;
            se.test140 = 5;
            se.test141 = 5;
            se.test142 = 5;
            se.test143 = 5;
            se.test144 = 5;
            se.test145 = 5;
            se.test146 = 5;
            se.test147 = 5;
            se.test148 = 5;
            se.test149 = 5;
            se.test150 = 5;
            se.test151 = 5;
            se.test152 = 5;
            se.test153 = 5;
            se.test154 = 5;
            se.test155 = 5;
            se.test156 = 5;
            se.test157 = 5;
            se.test158 = 5;
            se.test159 = 5;
            se.test160 = 5;
            se.test161 = 5;
            se.test162 = 5;
            se.test163 = 5;
            se.test164 = 5;
            se.test165 = 5;
            se.test166 = 5;
            se.test167 = 5;
            se.test168 = 5;
            se.test169 = 5;
            outputtime("Time to Run SuperExpando, set 50 test key/value pairs -- (not even setting values, just returning true from TrySetMember!)");

            dynamic eo = new ExpandoObject();
            outputtime("Declared ExpandoObject");
            eo.test120 = 5;
            eo.test121 = 5;
            eo.test122 = 5;
            eo.test123 = 5;
            eo.test124 = 5;
            eo.test125 = 5;
            eo.test126 = 5;
            eo.test127 = 5;
            eo.test128 = 5;
            eo.test129 = 5;
            eo.test130 = 5;
            eo.test131 = 5;
            eo.test132 = 5;
            eo.test133 = 5;
            eo.test134 = 5;
            eo.test135 = 5;
            eo.test136 = 5;
            eo.test137 = 5;
            eo.test138 = 5;
            eo.test139 = 5;
            eo.test140 = 5;
            eo.test141 = 5;
            eo.test142 = 5;
            eo.test143 = 5;
            eo.test144 = 5;
            eo.test145 = 5;
            eo.test146 = 5;
            eo.test147 = 5;
            eo.test148 = 5;
            eo.test149 = 5;
            eo.test150 = 5;
            eo.test151 = 5;
            eo.test152 = 5;
            eo.test153 = 5;
            eo.test154 = 5;
            eo.test155 = 5;
            eo.test156 = 5;
            eo.test157 = 5;
            eo.test158 = 5;
            eo.test159 = 5;
            eo.test160 = 5;
            eo.test161 = 5;
            eo.test162 = 5;
            eo.test163 = 5;
            eo.test164 = 5;
            eo.test165 = 5;
            eo.test166 = 5;
            eo.test167 = 5;
            eo.test168 = 5;
            eo.test169 = 5;
            outputtime("Time to Run ExpandoObject, set 50 test key/value pairs");
        }
    }
}

1 ответ

Решение

Во-первых, вы не должны измерять время, как это. DateTime.Now не является точным с точностью до миллисекунд. Вы должны использовать Stopwatch за это.

Во-вторых, в.Net в целом и при работе с dynamic особенно порядок имеет значение. Это потому, что есть несколько вещей, которые CLR и DLR должны вычислять в первый раз, но могут извлекать из кэша во второй раз.

В-третьих, в моем тестировании я, конечно, не видел 750 мс или что-то подобное.

Если я бегу SuperExpando во-первых, и сделать оба теста дважды, я получаю такие времена:

SuperExpando: 50,7736 ms
EpandoObject: 27,786 ms
SuperExpando: 0,0285 ms
EpandoObject: 0,0373 ms

Так, SuperExpando медленнее, и разница может быть значительной, но это только в первый раз. Когда вы снова запускаете тот же код с тем же типом, это намного быстрее.

Что происходит, когда мы меняем порядок?

EpandoObject: 33,3107 ms
SuperExpando: 43,7383 ms
EpandoObject: 0,0348 ms
SuperExpando: 0,0186 ms

SuperExpando все еще медленнее, но разница теперь меньше. И снова, второй прогон для обоих происходит быстрее на несколько порядков.

Другие вопросы по тегам