Оновлення: Додано попередньо складені та ліниво складені орієнтири
Оновлення 2: Виходить, я помиляюся. Повну і правильну відповідь див. У публікації Еріка Ліпперта. Я залишаю це тут заради показників орієнтирів
* Оновлення 3: Додано еталони IL- Eвидання та ледачий IL- викид на основі відповіді Марка Гравелла на це запитання .
Наскільки мені відомо, використання 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.4248ms
Динамічний: 45.0728ms
Віддзеркалення: 888.4011ms
Попередньо складений: 21.9166ms
LazyCompiled: 30.2045ms
ILEmitted: 8.4918ms
LazyILEвидано: 14.3483ms
Тож, використовуючи dynamic
ключове слово, на порядок більше, ніж виклик методу, він все-таки вдається завершити операцію мільйон разів за 50 мілісекунд, що робить його набагато швидшим, ніж відображення. Якби метод, який ми викликаємо, намагався зробити щось інтенсивне, наприклад поєднання декількох рядків разом або пошук колекції за значенням, ці операції, ймовірно, значно перевершать різницю між прямим викликом і dynamic
викликом.
Продуктивність - лише одна з багатьох вагомих причин не використовувати dynamic
зайвих, але якщо ви маєте справу з справді dynamic
даними, це може забезпечити переваги, що значно переважають недоліки.
Оновлення 4
На основі коментаря Johnbot я розбив область Reflection на чотири окремі тести:
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();
}),
... і ось базові результати:
Отже, якщо ви можете заздалегідь визначити певний метод, який вам потрібно буде зателефонувати багато, виклик кешованого делегата, що посилається на цей метод, настільки ж швидкий, як і виклик самого методу. Однак, якщо вам потрібно визначити, який метод викликати так само, як ви збираєтеся його викликати, створення делегата для нього дуже дорого.