Aktualizacja: Dodano wstępnie skompilowane i leniwie skompilowane testy porównawcze
Aktualizacja 2: Okazuje się, że się mylę. Pełna i poprawna odpowiedź znajduje się w poście Erica Lipperta. Zostawiam to tutaj ze względu na wartości wzorcowe
* Aktualizacja 3: Dodano testy porównawcze Emitowane przez IL i Leniwe emisje IL, w oparciu o odpowiedź Marka Gravella na to pytanie .
O ile mi wiadomo, użycie dynamic
słowa kluczowego samo w sobie nie powoduje żadnej dodatkowej kompilacji w czasie wykonywania (chociaż wyobrażam sobie, że mogłoby to zrobić w określonych okolicznościach, w zależności od typu obiektów, które stanowią kopię zapasową zmiennych dynamicznych).
Jeśli chodzi o wydajność, z dynamic
natury wprowadza pewne narzuty, ale nie tak bardzo, jak mogłoby się wydawać. Na przykład właśnie przeprowadziłem test porównawczy, który wygląda następująco:
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>));
}
Jak widać z kodu, próbuję wywołać prostą metodę no-op na siedem różnych sposobów:
- Bezpośrednie wywołanie metody
- Za pomocą
dynamic
- Przez refleksję
- Użycie elementu,
Action
który został wstępnie skompilowany w czasie wykonywania (w ten sposób wykluczając czas kompilacji z wyników).
- Użycie
Action
zmiennej, która jest kompilowana za pierwszym razem, gdy jest potrzebna, przy użyciu zmiennej Lazy, która nie jest bezpieczna dla wątków (w ten sposób obejmuje czas kompilacji)
- Korzystanie z dynamicznie generowanej metody, która jest tworzona przed testem.
- Korzystanie z dynamicznie generowanej metody, która jest leniwie tworzona podczas testu.
Każdy jest wywoływany milion razy w prostej pętli. Oto wyniki pomiaru czasu:
Bezpośredni: 3,4248 ms
Dynamiczny: 45,0728 ms
Odbicie: 888,4011 ms
Prekompilowany: 21,9166
ms
LazyCompiled: 30,2045
ms ILE,
Tak więc, chociaż użycie dynamic
słowa kluczowego trwa o rząd wielkości dłużej niż bezpośrednie wywołanie metody, nadal udaje mu się zakończyć operację milion razy w około 50 milisekund, co czyni ją znacznie szybszą niż odbicie. Gdyby wywoływana przez nas metoda próbowała wykonać coś intensywnego, na przykład połączyć kilka ciągów razem lub wyszukać w kolekcji wartość, operacje te prawdopodobnie znacznie przeważyłyby nad różnicą między wywołaniem bezpośrednim a dynamic
wywołaniem.
Wydajność to tylko jeden z wielu dobrych powodów, dla których nie należy używać ich dynamic
niepotrzebnie, ale gdy masz do czynienia z prawdziwymi dynamic
danymi, może zapewnić korzyści znacznie przewyższające wady.
Zaktualizuj 4
Opierając się na komentarzu Johnbota, podzieliłem obszar refleksji na cztery oddzielne testy:
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();
}),
... a oto wyniki testów porównawczych:
Jeśli więc możesz z góry określić konkretną metodę, którą będziesz musiał często wywoływać, wywołanie delegata z pamięci podręcznej odnoszącego się do tej metody jest prawie tak szybkie, jak wywołanie samej metody. Jeśli jednak chcesz określić, którą metodę wywołać w momencie, gdy masz zamiar ją wywołać, utworzenie delegata jest bardzo kosztowne.