Pull to refresh

Динамические вызовы: сравнение методов

Reading time 15 min
Views 8.7K

Динамические вызовы: что это и зачем?



Думаю, для каждого разработчика, работающим на статических языках программирования, иногда возникала необходимость прибегнуть к динамическим вызовам — вызвать метод чего-то, о чем пока еще ничего не известно. Или получить какое-то свойство у какого-то объекта, о котором будет известно только в run-time.

Это иногда используется в алгоритмах, основанных на так называемой «утиной типизации» (duck typing):
Если что-то выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть.


В данной статье я хотел бы рассмотреть основные доступные в Microsoft .NET 4.0 способы, сравнить их производительность и синтаксис.


Основные доступные варианты



Сначала я перечислю те варианты, которые я проанализирую в этой статье. Если какой-то из вариантов покажется не интересным, просто пропускайте. И если будет еще какие либо варианты, стоящие рассмотрения, напишите, пожалуйста, я их добавлю в текст.

Сразу предупрежу, что рассмотренные методы не являются полностью взаимно-заменяемыми: у каждого есть своя особенность и сфера применения. Я хотел сравнить сложность использования и скорость доступа/вызова динамических методов. Это даст возможность выбрать тот или иной метод исходя из конкретной задачи.

Итак:

— Рефлексия (Reflection, RTTI, интроспекция)
— Словарь значений
— Словарь делегатов
— Динамический объект со статическим контейнером
— Динамический объект с Expando
— Динамический объект с перехватом вызова
— Компиляция выражений (Expressions)

Методика тестирования



Так, как цель — сравнить скорость доступа к полям/методам, то тесты я оформил в такую общую структуру:
— есть тестирующий объект-контейнер (ObjectTester), который содержит в себе тестируемый объект
— тестируемый объект должен содержать два свойства — одно целое (A) и одно строковое (B)
— тестирующий объект делегирует обращения к A и B тестируемому объекту
— тестовая среда создает массив из 2000 объектов контейнеров, заполняя их данными
— тестовая среда выполняет тестирующую операцию 2000 раз над каждым объектом в массиве
— тестирующая операция должна как минимум два раза получить значение свойств А и B (два раза для того, чтоб дать возможность проверить наличие кэша в решении)
— для каждого метода тестирования тест запускается 5 раз

Код компилируется под Release конфигурацией.
Тесты запускались на Windows 7 x64, .NET 4.0.30319, Intel Core 2 Quad Q9400 (2666 MHz), 4GB DDR2

Статическая (не динамическая) реализация


Для сравнения, как точка отсчета добавлен такой ObjectTester, который содержит в себе простой класс с двумя авто-свойствами и делегирующий вызовы этому классу.

Данный метод будет самым быстрым, так как совершенно не динамический. Поэтому мы будем сравнивать другие методы по отношению к этому.

Код довольно простой (кстати, запомните статический класс, мы еще им воспользуемся):
private class StaticObject
    {
      public int A { get; set; }
      public string B { get; set; }
    }
    
    private class StaticObjectTester : ObjectTester
    {
      private readonly StaticObject _o = new StaticObject();

      public override int A
      {
        get { return _o.A; }
        set { _o.A = value; }
      }

      public override string B
      {
        get { return _o.B; }
        set { _o.B = value; }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Результат: 00:00:06.6274538 (+0,00%)

Плюсы:
— Максимально быстро
Минусы
— Не динамически

Рефлексия (Reflection, RTTI, интроспекция)



Думаю, это самый первый способ, который приходит на ум, когда надо динамические вызовы.

Для того, чтоб максимально ускорить, я использовал статическое кэширование PropertyInfo. Обычно в реальных проектах всегда есть возможность хоть как-то закэшировать информацию, необходимую для рефлексии.

Так, как нас интересует скорость работы именно рефлексии, то для хранения данных воспользуемся уже готовым классом с двумя свойствами.

Код тестирующего объекта:
private class ReflectionObjectTester : ObjectTester
    {
      private static readonly PropertyInfo PropertyA = typeof (StaticObject).GetProperty("A");
      private static readonly PropertyInfo PropertyB = typeof (StaticObject).GetProperty("B");
      private static readonly object[] Empty = new object[0];
      private readonly Object _o = new StaticObject();

      public override int A
      {
        get { return (int) PropertyA.GetValue(_o, Empty); }
        set { PropertyA.SetValue(_o, value, Empty); }
      }

      public override string B
      {
        get { return (string) PropertyB.GetValue(_o, Empty); }
        set { PropertyB.SetValue(_o, value, Empty); }
      }
    }


* This source code was highlighted with Source Code Highlighter.



Результат: 00:01:32.3217880 (+1293,02%)

Фактически использование рефлексии увеличило время исполнения почти в 13 раз! Хороший способ, чтоб задуматься перед тем, как использовать этот метод.

Плюсы:
— полностью run time
Минусы:
— слишком долго
— могут возникать проблемы из-за требования прав «ReflectionPermission»

Словарь значений



Смысл метода — просто хранить значения в словаре (Dictionary<TKey,TValue>). Как ключ возьмем имя поля, как значение — значение поля. Придется использовать Object как тип значения.

Код:
private class DictionaryObjectTester : ObjectTester
    {
      private const string AName = "A";
      private const string BName = "B";
      private readonly Dictionary<string, Object> _o = new Dictionary<string, object>();

      public override int A
      {
        get { return (int) _o[AName]; }
        set { _o[AName] = value; }
      }

      public override string B
      {
        get { return (string) _o[BName]; }
        set { _o[BName] = value; }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Результат: 00:00:10.8516518 (+63,64%)

Плюсы:
— совсем динамически: можно и добавлять и удалять
Минусы:
— слабая типизация значений (Object)
— только значения, а не методы

Словарь делегатов



На практике чаще используется немного усложненный вариант — мы храним не значения, а способы получения этих значений. Или просто некий динамический набор функций/действий.

Реализация:
private class DictionaryDelegateTester : ObjectTester
    {
      private const string AName = "A";
      private const string BName = "B";

      private readonly Dictionary<string, Func<Object>> _getters;      
      private readonly Dictionary<string, Action<Object>> _setters;

      private readonly StaticObject _o = new StaticObject();

      public DictionaryDelegateTester()
      {
        _getters = new Dictionary<string, Func<Object>>
                {
                  {AName, () => _o.A},
                  {BName, () => _o.B}
                };
        _setters = new Dictionary<string, Action<object>>
                {
                  {AName, v => _o.A = (int) v},
                  {BName, v => _o.B = v.ToString()},
                };
      }

      public override int A
      {
        get { return (int) _getters[AName](); }
        set { _setters[AName](value); }
      }

      public override string B
      {
        get { return (string) _getters[BName](); }
        set { _setters[BName](value); }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Результат:00:00:12.3299023 (+85,93%)

Плюсы:
— совсем динамически: можно и добавлять и удалять
Минусы:
— сложный синтаксис

Динамический объект со статическим контейнером



Ну вот и переходим к тому самому новому типу 'dynamic'. Для него я сразу три метода применю. Первый — когда мы храним тот самый статический тип как «хранилище» (backed object) для dynamic'а:

    private class DynamicObjectTester : ObjectTester
    {
      private readonly dynamic _o = new StaticObject();

      public override int A
      {
        get { return _o.A; }
        set { _o.A = value; }
      }

      public override string B
      {
        get { return _o.B; }
        set { _o.B = value; }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Результат: 00:00:10.7193446 (+61,65%)

Плюсы:
— может передаваться в DLR (F#)
— простой синтаксис
Минусы:
— и все-таки в полтора раза дольше

Динамический объект с Expando


Вообще-то для того, чтоб удобней использовать dynamic-и добавили тип ExpandoObject. У него есть много приятных мелочей — он и IDictionary, и IEnumerable.

Использование:
    private class ExpandoDynamicObjectTester : ObjectTester
    {
      private readonly dynamic _o = new ExpandoObject();

      public override int A
      {
        get { return _o.A; }
        set { _o.A = value; }
      }

      public override string B
      {
        get { return _o.B; }
        set { _o.B = value; }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Результат: 00:00:09.7034082 (+46,33%)

Плюсы:
— может передаваться в DLR (F#)
— простой синтаксис и дополнительные возможности (энумерация)
Минусы:
— практически отсутствуют, кроме прибавки в скорости

Динамический объект с перехватом вызова



Еще одна из приятных возможностей dynamic — возможность перехватывать вызовы к методам и свойствам. Воспользуемся:
    private class PureDynamicObjectTester : ObjectTester
    {
      private readonly dynamic _o = new DynamicContainer();

      public override int A
      {
        get { return _o.A; }
        set { _o.A = value; }
      }

      public override string B
      {
        get { return _o.B; }
        set { _o.B = value; }
      }

      #region Nested type: DynamicContainer

      private class DynamicContainer : DynamicObject
      {
        private int _a;
        private string _b;

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
          if (binder.Name == "A")
          {
            result = _a;
            return true;
          }
          if (binder.Name == "B")
          {
            result = _b;
            return true;
          }
          return base.TryGetMember(binder, out result);
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
          if (binder.Name == "A")
          {
            _a = (int) value;
            return true;
          }
          if (binder.Name == "B")
          {
            _b = (string) value;
            return true;
          }
          return base.TrySetMember(binder, value);
        }
      }

      #endregion
    }


* This source code was highlighted with Source Code Highlighter.


Немного длинновато вышло.

Результат: 00:00:11.1040041 (+67,45%)

Плюсы:
— может передаваться в DLR (F#)
— свойства могут даже не существовать
Минусы:
— требует существенно больше кода

Компиляция выражений (Expressions)



Этот метод не использует dynamic (Хотя довольно легко его можно обернуть в DynamicObject), и наиболее близко находится к методу с использованием рефлексии. На самом деле он и использует рефлексию, но только для того, чтоб построить дерево выражений и откомпилировать его.

Фактически происходит «эмитинг» IL-кода. В результате мы получаем просто огромную прибавку к скорости.

Имплементацию я написал такую: Сделал два метода-расширения над propertyInfo для получения собственно getter-а и setter-а, но не в виде MethodInfo как для обычной рефлексии, а в виде Func-а (getter) и Action-а (setter). Фактически геттер у меня выглядит примерно как "o => ((T)o).{name}" а сеттер — "(o, v) => ((T)o).{name} = v".

Чтоб оставить эти вспомогательные методы простыми для чтения, я каждый из узлов выражения в отдельные переменные поместил:
    public static Func<object, T> GetValueGetter<T>(this PropertyInfo propertyInfo)
    {      
      var instance = Expression.Parameter(typeof(Object), "i");
      var castedInstance = Expression.ConvertChecked(instance, propertyInfo.DeclaringType);
      var property = Expression.Property(castedInstance, propertyInfo);      
      var convert = Expression.Convert(property, typeof(T));
      var expression = Expression.Lambda(convert, instance);
      return (Func<object, T>)expression.Compile();
    }

    public static Action<object,T> GetValueSetter<T>(this PropertyInfo propertyInfo)
    {
      var instance = Expression.Parameter(typeof(Object), "i");
      var castedInstance = Expression.ConvertChecked(instance, propertyInfo.DeclaringType);      
      var argument = Expression.Parameter(typeof(T), "a");
      var setterCall = Expression.Call(
        castedInstance,
        propertyInfo.GetSetMethod(),
        Expression.Convert(argument, propertyInfo.PropertyType));
      return (Action<object,T>)Expression.Lambda(setterCall, instance, argument)
                        .Compile();
    }


* This source code was highlighted with Source Code Highlighter.


В итоге тестирующий класс:
    private class ExpressionObjectTester : ObjectTester
    {
      private static readonly Func<object, int> AGetter =
        typeof (StaticObject).GetProperty("A").GetValueGetter<int>();

      private static readonly Func<object, string> BGetter =
        typeof (StaticObject).GetProperty("B").GetValueGetter<string>();

      private static readonly Action<object, int> ASetter =
        typeof (StaticObject).GetProperty("A").GetValueSetter<int>();

      private static readonly Action<object, string> BSetter =
        typeof (StaticObject).GetProperty("B").GetValueSetter<string>();

      private readonly StaticObject _o = new StaticObject();

      public override int A
      {
        get { return AGetter(_o); }
        set { ASetter(_o, value); }
      }

      public override string B
      {
        get { return BGetter(_o); }
        set { BSetter(_o, value); }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Результат: 00:00:08.5675928 (+29,20%)

Плюсы:
— потрясающая скорость
Минусы
— немного сложная реализация

Сводная таблица и выводы



Название метода Время Добавка времени
Рефлексия 00:01:33.6077139 1311,59%
Словарь значений 00:00:10.8516518 63,64%
Словарь делегатов 00:00:12.3299023 85,93%
Динамический объект со статическим контейнером 00:00:10.7193446 61,65%
Динамический объект с Expando 00:00:09.7034082 46,33%
Динамический объект с перехватом вызова 00:00:11.1040041 67,45%
Компиляция выражений (Expressions) 00:00:08.5675928 29,20%


Выводы довольно простые:
— Если вам нужна скорость — используйте Expression-ы. Они не такие страшные, если немного покопать, но дают просто потрясающие результаты, если сравнивать с обычной рефлексией (хотя даже если с dynamic, то почти два раза быстрее в среднем)
— Если нужно использовать dynamic объекты (например, в связке с DLR — F#,IronRuby) то лучше использовать ExpandoObject. Он дает отличные результаты.
— Реализация dynamic-ов в .NET'е довольно эффективная, даже если сравнивать со словарем

Но самое главное: Используйте динамические вызовы только там, где они действительно нужны!

Хороший пример — обработка XML-данных со слабо-определенной структурой. Плохой пример — описание алгоритма. Лучше всегда выделить необходимые интерфейсы и уже оперировать понятиями, описывающими поведение.

Не забывайте — динамические типы практически сводят на нет возможность рефакторинга, возможность отлова ошибок на этапе компиляции, возможность эффективной оптимизации.

Надеюсь, эта статья поможет тем, кто думает воспользоватся динамическими возможностями языка — или выбрать правильный метод или принять решение не пользоваться.

Код доступен на Google Code для клонирования или просмотра
Tags:
Hubs:
+54
Comments 27
Comments Comments 27

Articles