Обновление: добавлены предварительно скомпилированные и лениво скомпилированные тесты.
Обновление 2: Оказывается, я ошибаюсь. Полный и правильный ответ см. В сообщении Эрика Липперта. Я оставляю это здесь ради контрольных цифр
* Обновление 3: добавлены тесты производительности IL-Emitted и Lazy IL-Emitted, основанные на ответе Марка Гравелла на этот вопрос .
Насколько мне известно, использование dynamic
ключевого слова само по себе не вызывает дополнительной компиляции во время выполнения (хотя я полагаю, что это могло бы происходить при определенных обстоятельствах, в зависимости от того, какой тип объектов поддерживает ваши динамические переменные).
Что касается производительности, dynamic
он по своей сути вносит некоторые накладные расходы, но не так сильно, как вы думаете. Например, я только что запустил тест, который выглядит так:
void Main()
{
Foo foo = new Foo();
var args = new object[0];
var method = typeof(Foo).GetMethod("DoSomething");
dynamic dfoo = foo;
var precompiled =
Expression.Lambda<Action>(
Expression.Call(Expression.Constant(foo), method))
.Compile();
var lazyCompiled = new Lazy<Action>(() =>
Expression.Lambda<Action>(
Expression.Call(Expression.Constant(foo), method))
.Compile(), false);
var wrapped = Wrap(method);
var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
var actions = new[]
{
new TimedAction("Direct", () =>
{
foo.DoSomething();
}),
new TimedAction("Dynamic", () =>
{
dfoo.DoSomething();
}),
new TimedAction("Reflection", () =>
{
method.Invoke(foo, args);
}),
new TimedAction("Precompiled", () =>
{
precompiled();
}),
new TimedAction("LazyCompiled", () =>
{
lazyCompiled.Value();
}),
new TimedAction("ILEmitted", () =>
{
wrapped(foo, null);
}),
new TimedAction("LazyILEmitted", () =>
{
lazyWrapped.Value(foo, null);
}),
};
TimeActions(1000000, actions);
}
class Foo{
public void DoSomething(){}
}
static Func<object, object[], object> Wrap(MethodInfo method)
{
var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
typeof(object), typeof(object[])
}, method.DeclaringType, true);
var il = dm.GetILGenerator();
if (!method.IsStatic)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
}
var parameters = method.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
}
il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
OpCodes.Call : OpCodes.Callvirt, method, null);
if (method.ReturnType == null || method.ReturnType == typeof(void))
{
il.Emit(OpCodes.Ldnull);
}
else if (method.ReturnType.IsValueType)
{
il.Emit(OpCodes.Box, method.ReturnType);
}
il.Emit(OpCodes.Ret);
return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}
Как видно из кода, я пытаюсь вызвать простой бездействующий метод семью разными способами:
- Прямой вызов метода
- С помощью
dynamic
- По размышлению
- Использование,
Action
которое было предварительно скомпилировано во время выполнения (таким образом, исключая время компиляции из результатов).
- Использование,
Action
которое компилируется в первый раз, когда это необходимо, с использованием небезопасной для потоков переменной Lazy (включая время компиляции)
- Использование динамически сгенерированного метода, который создается перед тестом.
- Использование динамически генерируемого метода, который лениво создается во время теста.
Каждый вызывается 1 миллион раз в простом цикле. Вот результаты тайминга:
Прямое: 3,4248 мс
Динамическое: 45,0728 мс
Отражение: 888,4011 мс
Предварительно скомпилированное: 21,9166
мс LazyCompiled: 30,2045
мс ILEmitted: 8,4918
мс LazyILEmitted: 14,3483 мс
Таким образом, хотя использование dynamic
ключевого слова занимает на порядок больше времени, чем прямой вызов метода, ему все же удается выполнить операцию миллион раз примерно за 50 миллисекунд, что делает ее намного быстрее, чем отражение. Если бы вызываемый нами метод пытался сделать что-то интенсивное, например, объединение нескольких строк или поиск значения в коллекции, эти операции, вероятно, намного перевесили бы разницу между прямым вызовом и dynamic
вызовом.
Производительность - лишь одна из многих веских причин не использовать их dynamic
без надобности, но когда вы имеете дело с настоящими dynamic
данными, это может дать преимущества, которые намного перевешивают недостатки.
Обновление 4
Основываясь на комментарии Джонбота, я разбил область отражения на четыре отдельных теста:
new TimedAction("Reflection, find method", () =>
{
typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
}),
new TimedAction("Reflection, predetermined method", () =>
{
method.Invoke(foo, args);
}),
new TimedAction("Reflection, create a delegate", () =>
{
((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
}),
new TimedAction("Reflection, cached delegate", () =>
{
methodDelegate.Invoke();
}),
... и вот результаты тестов:
Поэтому, если вы можете заранее определить конкретный метод, который вам нужно будет вызывать много раз, вызов кешированного делегата, ссылающегося на этот метод, примерно такой же быстрый, как и вызов самого метода. Однако, если вам нужно определить, какой метод вызывать, как только вы собираетесь его вызвать, создание делегата для него очень дорого.