У меня вопрос по производительности dynamic
в C #. Я читал, dynamic
заставляет компилятор снова работать, но что он делает?
Нужно ли перекомпилировать весь метод с dynamic
переменной, используемой в качестве параметра, или только те строки с динамическим поведением / контекстом?
Я заметил, что использование 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));
}
c#
performance
dynamic
Лукаш Мадон
источник
источник
Ответы:
Вот сделка.
Для каждого выражения в вашей программе, имеющего динамический тип, компилятор генерирует код, который генерирует один «объект сайта динамического вызова», представляющий операцию. Так, например, если у вас есть:
тогда компилятор сгенерирует морально подобный код. (Фактический код немного сложнее; он упрощен для целей презентации.)
Видите, как это работает до сих пор? Мы генерируем сайт вызова один раз , независимо от того, сколько раз вы звоните M. Сайт вызова живет вечно после того, как вы его сгенерируете один раз. Сайт вызова - это объект, который представляет «здесь будет динамический вызов Foo».
Итак, теперь, когда у вас есть сайт вызова, как работает вызов?
Сайт вызова является частью среды выполнения динамического языка. DLR говорит: «Хм, кто-то пытается выполнить динамический вызов метода foo для этого объекта here. Знаю ли я что-нибудь об этом? Нет. Тогда мне лучше выяснить».
Затем DLR опрашивает объект в d1, чтобы увидеть, есть ли в нем что-нибудь особенное. Может быть, это устаревший COM-объект, или объект Iron Python, или объект Iron Ruby, или объект IE DOM. Если это не так, то это должен быть обычный объект C #.
Это момент, когда компилятор снова запускается. Нет необходимости в лексере или парсере, поэтому DLR запускает специальную версию компилятора C #, в которой есть только анализатор метаданных, семантический анализатор выражений и эмиттер, который генерирует деревья выражений вместо IL.
Анализатор метаданных использует Reflection для определения типа объекта в d1, а затем передает это семантическому анализатору, чтобы узнать, что происходит, когда такой объект вызывается в методе Foo. Анализатор разрешения перегрузки вычисляет это, а затем строит дерево выражений - точно так же, как если бы вы вызвали Foo в лямбда-выражении дерева выражений - которое представляет этот вызов.
Затем компилятор C # передает это дерево выражений обратно в DLR вместе с политикой кэширования. Политика обычно такова: «когда вы второй раз видите объект этого типа, вы можете повторно использовать это дерево выражений, а не перезванивать мне». Затем DLR вызывает Compile для дерева выражений, который вызывает компилятор дерева выражений в IL и выдает блок динамически сгенерированного IL в делегате.
Затем DLR кэширует этого делегата в кэше, связанном с объектом сайта вызова.
Затем он вызывает делегата, и происходит вызов Foo.
Во второй раз, когда вы звоните М, у нас уже есть место для звонков. DLR снова опрашивает объект, и если объект того же типа, что и в прошлый раз, он извлекает делегата из кеша и вызывает его. Если объект другого типа, кеш пропускается, и весь процесс начинается заново; делаем семантический анализ звонка и сохраняем результат в кеше.
Это происходит для каждого выражения , связанного с динамикой. Так, например, если у вас есть:
затем есть три сайта динамических вызовов. Один для динамического вызова Foo, один для динамического добавления и один для динамического преобразования из dynamic в int. Каждый из них имеет свой собственный анализ времени выполнения и свой собственный кеш результатов анализа.
Есть смысл?
источник
Обновление: добавлены предварительно скомпилированные и лениво скомпилированные тесты.
Обновление 2: Оказывается, я ошибаюсь. Полный и правильный ответ см. В сообщении Эрика Липперта. Я оставляю это здесь ради контрольных цифр
* Обновление 3: добавлены тесты производительности IL-Emitted и Lazy IL-Emitted, основанные на ответе Марка Гравелла на этот вопрос .
Насколько мне известно, использованиеdynamic
ключевого слова само по себе не вызывает дополнительной компиляции во время выполнения (хотя я полагаю, что это могло бы происходить при определенных обстоятельствах, в зависимости от того, какой тип объектов поддерживает ваши динамические переменные).Что касается производительности,
dynamic
он по своей сути вносит некоторые накладные расходы, но не так сильно, как вы думаете. Например, я только что запустил тест, который выглядит так:Как видно из кода, я пытаюсь вызвать простой бездействующий метод семью разными способами:
dynamic
Action
которое было предварительно скомпилировано во время выполнения (таким образом, исключая время компиляции из результатов).Action
которое компилируется в первый раз, когда это необходимо, с использованием небезопасной для потоков переменной Lazy (включая время компиляции)Каждый вызывается 1 миллион раз в простом цикле. Вот результаты тайминга:
Таким образом, хотя использование
dynamic
ключевого слова занимает на порядок больше времени, чем прямой вызов метода, ему все же удается выполнить операцию миллион раз примерно за 50 миллисекунд, что делает ее намного быстрее, чем отражение. Если бы вызываемый нами метод пытался сделать что-то интенсивное, например, объединение нескольких строк или поиск значения в коллекции, эти операции, вероятно, намного перевесили бы разницу между прямым вызовом иdynamic
вызовом.Производительность - лишь одна из многих веских причин не использовать их
dynamic
без надобности, но когда вы имеете дело с настоящимиdynamic
данными, это может дать преимущества, которые намного перевешивают недостатки.Обновление 4
Основываясь на комментарии Джонбота, я разбил область отражения на четыре отдельных теста:
... и вот результаты тестов:
Поэтому, если вы можете заранее определить конкретный метод, который вам нужно будет вызывать много раз, вызов кешированного делегата, ссылающегося на этот метод, примерно такой же быстрый, как и вызов самого метода. Однако, если вам нужно определить, какой метод вызывать, как только вы собираетесь его вызвать, создание делегата для него очень дорого.
источник
dynamic
конечно проигрышем:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);