Обновление: добавлены предварительно скомпилированные и лениво скомпилированные тесты
Обновление 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>));
}
Как видно из кода, я пытаюсь вызвать простой метод no-op семью различными способами:
- Прямой вызов метода
- Использование
dynamic
- Отражением
- Использование
Action
, предварительно скомпилированного во время выполнения (таким образом, исключая время компиляции из результатов).
- Использование
Action
, которое компилируется при первой необходимости, с использованием не-поточно-безопасной переменной Lazy (таким образом, включая время компиляции)
- Использование динамически генерируемого метода, который создается перед тестом.
- Использование динамически генерируемого метода, который лениво создается во время теста.
Каждый вызывается 1 миллион раз в простом цикле. Вот временные результаты:
Прямой: 3,4248 мс
Динамический: 45.0728мс
Отражение: 888,4011мс
Предварительно скомпилировано: 21,9166мс
LazyCompiled: 30.2045мс
ILEmitted: 8,4918мс
ЛенивыйВыпущено: 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();
}),
... и вот результаты теста:
Так что, если вы можете заранее определить конкретный метод, который вам нужно будет вызывать много, вызов кэшированного делегата, ссылающегося на этот метод, примерно так же быстр, как и вызов самого метода. Однако, если вам нужно определить, какой метод вызывать так же, как вы собираетесь его вызывать, создание делегата для него очень дорого.