Как наличие динамической переменной влияет на производительность?
У меня есть вопрос о производительности dynamic в C#. Я читал, что dynamic снова запускает компилятор, но что он делает?
должен ли он перекомпилировать весь метод с динамической переменной, используемой в качестве параметра, или только те строки с динамическим поведением/контекстом?
Я заметил, что использование динамических переменных может замедлить простой цикл for на 2 порядка.
код, с которым я играл:
internal class Sum2
{
public int intSum;
}
internal class Sum
{
public dynamic DynSum;
public int intSum;
}
class Program
{
private const int ITERATIONS = 1000000;
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
dynamic param = new Object();
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
Console.ReadKey();
}
private static void Sum(Stopwatch stopwatch)
{
var sum = 0;
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch, dynamic param)
{
var sum = new Sum2();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
}
private static void DynamicSum(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.DynSum += i;
}
stopwatch.Stop();
Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
2 ответов:
Я прочитал динамический заставляет компилятор работать снова, но то, что он делает. Должен ли он перекомпилировать весь метод с динамикой, используемой в качестве параметра, или, скорее, те строки с динамическим поведением / контекстом(?)
вот в чем дело.
для каждого выражение в вашей программе, которая имеет динамический тип, компилятор выдает код, который генерирует один "объект сайта динамического вызова", представляющий операцию. Так, например, если вы есть:
class C { void M() { dynamic d1 = whatever; dynamic d2 = d1.Foo();тогда компилятор будет генерировать код, который морально такой. (Фактический код довольно немного сложнее; это упрощено для целей представления.)
class C { static DynamicCallSite FooCallSite; void M() { object d1 = whatever; object d2; if (FooCallSite == null) FooCallSite = new DynamicCallSite(); d2 = FooCallSite.DoInvocation("Foo", d1);посмотрим, как это работает до сих пор? Мы генерируем сайт вызова после, независимо от того, сколько раз вы звоните М. сайт вызова живет вечно после того, как вы создадите его один раз. Сайт вызова-это объект, который представляет собой " будет динамический вызов Foo здесь."
Итак, теперь, когда у вас есть сайт вызова, как работает вызов?
сайт вызова является частью динамической языковой среды выполнения. DLR говорит: "Хм, кто-то пытается сделать динамический вызов метода foo на этом объекте here. Я что-нибудь об этом знаю? Нет. Тогда мне лучше узнать."
DLR затем опрашивает объект в d1, чтобы увидеть, если это что-то особенное. Возможно, это устаревший COM-объект или объект Iron Python, или Объект утюг Руби, или т. е. DOM-объекта. Если это не любой из них, то это должен быть обычный объект C#.
это точка, где компилятор запускается снова. Нет необходимости в лексере или синтаксическом анализаторе, поэтому DLR запускает специальную версию компилятора C#, которая имеет только анализатор метаданных, семантический анализатор выражений и эмиттер, который выдает деревья выражений вместо IL.
анализатор метаданных использует отражение для определения типа объекта в d1, а затем передает это семантическому анализатору, чтобы спросить, что происходит, когда такой объект вызывается на метод Foo. Анализатор разрешения перегрузки вычисляет это, а затем строит дерево выражений-так же, как если бы Вы вызвали Foo в дереве выражений lambda-который представляет этот вызов.
компилятор C# затем передает это дерево выражений обратно в DLR вместе с политикой кэша. Политики обычно "второй раз, когда вы видите объект этого типа, вы можете повторно использовать этот дерево выражений вместо того, чтобы призвать меня обратно". Затем DLR вызывает Compile в дереве выражений, которое вызывает компилятор expression-tree-to-IL и выплевывает блок динамически генерируемого IL в делегате.
DLR затем кэширует этот делегат в кэше, связанном с объектом сайта вызова.
затем он вызывает делегат, и происходит вызов Foo.
во второй раз, когда вы звоните M, у нас уже есть сайт вызова. DLR опрашивает объект опять же, если объект имеет тот же тип, что и в прошлый раз, он извлекает делегат из кэша и вызывает его. Если объект имеет другой тип, то кэш пропускает, и весь процесс начинается снова; мы делаем семантический анализ вызова и сохраняем результат в кэше.
это произойдет для выражение это связано с динамикой. Так, например, если у вас есть:
int x = d1.Foo() + d2;то есть три динамические сайты вызовов. Один для динамического вызова Foo, один для динамического добавления и один для динамического преобразования из dynamic в int. Каждый из них имеет свой собственный анализ времени выполнения и свой собственный кэш результатов анализа.
смысл?
обновление: добавлены предварительно скомпилированные и лениво скомпилированные тесты
обновление 2: Оказывается, я ошибаюсь. Эрик Липперт для полного и правильного ответа. Я оставляю это здесь ради контрольных цифр
*обновление 3: добавлены Il-излучаемые и ленивые 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>)); }как вы можете видеть из кода, я пытаюсь вызвать простой метод no-op seven разные способы:
- прямой вызов метода
- используя
dynamic- рефлексия
- С помощью
Actionкоторый был предварительно скомпилирован во время выполнения (таким образом, исключая время компиляции из результатов).- С помощью
Actionкоторый компилируется в первый раз, когда это необходимо, используя не потокобезопасную ленивую переменную (таким образом, включая время компиляции)- использование динамически сгенерированного метода, который создается до тест.
- использование динамически генерируемого метода, который лениво создается во время теста.
каждый вызывается 1 миллион раз в простом цикле. Вот результаты:
прямой: 3,4248 МС
Динамика: 45.0728 МС
Отражение: 888.4011 МС
Предварительно скомпилированный: 21.9166 МС
LazyCompiled: 30.2045 ms
ILEmitted: 8.4918 ms
LazyILEmitted: 14.3483 msтак при использовании
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(); }),... и вот результаты тестов:
так что если вы можете предопределить конкретный метод, который вам нужно будет вызвать много, вызов кэшированного делегата, ссылающегося на этот метод, примерно так же быстро, как вызов самого метода. Однако, если вам нужно определить, какой метод вызывать так же, как вы собираетесь вызвать его, создание делегата для него очень дорого.

Comments