Динамические вызовы: что это и зачем?
Думаю, для каждого разработчика, работающим на статических языках программирования, иногда возникала необходимость прибегнуть к динамическим вызовам — вызвать метод чего-то, о чем пока еще ничего не известно. Или получить какое-то свойство у какого-то объекта, о котором будет известно только в 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 для клонирования или просмотра