Как наличие динамической переменной влияет на производительность? - PullRequest
113 голосов
/ 20 сентября 2011

У меня есть вопрос по поводу производительности dynamic в C #.Я прочитал dynamic, что заставляет компилятор снова работать, но что он делает?

Нужно ли перекомпилировать весь метод с переменной dynamic, используемой в качестве параметра, или только те строки с динамическим поведением?/ context?

Я заметил, что использование 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 ]

216 голосов
/ 20 сентября 2011

Я прочитал динамически, заставляет компилятор снова работать, но что он делает.Нужно ли перекомпилировать весь метод с динамическим, используемым в качестве параметра, или, скорее, те строки с динамическим поведением / контекстом (?)

Вот предложение.

Для каждого Выражение в вашей программе динамического типа компилятор генерирует код, который генерирует один «объект сайта динамического вызова», представляющий операцию.Так, например, если у вас есть:

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 для этого здесь объекта. Знаю ли я что-нибудь об этом? Нет. Тогда я лучше выясню».

Тогда 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.

Во второй раз, когда вы звоните M, у нас уже есть сайт вызова.DLR снова запрашивает объект, и если объект того же типа, что и в прошлый раз, он выбирает делегата из кэша и вызывает его.Если объект другого типа, то кеш пропадает, и весь процесс начинается заново;мы делаем семантический анализ вызова и сохраняем результат в кеше.

Это происходит для каждого выражения , которое включает динамическое.Например, если у вас есть:

int x = d1.Foo() + d2;

, то есть три сайта с динамическими вызовами.Один для динамического вызова Foo, один для динамического добавления и один для динамического преобразования из динамического в int.Каждый из них имеет свой собственный анализ времени выполнения и свой собственный кэш результатов анализа.

Имеет смысл?

97 голосов
/ 20 сентября 2011

Обновление: добавлены предварительно скомпилированные и лениво скомпилированные тесты

Обновление 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 семью различными способами:

  1. Прямой вызов метода
  2. Использование dynamic
  3. Отражением
  4. Использование Action, предварительно скомпилированного во время выполнения (таким образом, исключая время компиляции из результатов).
  5. Использование Action, которое компилируется при первой необходимости, с использованием не-поточно-безопасной переменной Lazy (таким образом, включая время компиляции)
  6. Использование динамически генерируемого метода, который создается перед тестом.
  7. Использование динамически генерируемого метода, который лениво создается во время теста.

Каждый вызывается 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();
    }),

... и вот результаты теста:

enter image description here

Так что, если вы можете заранее определить конкретный метод, который вам нужно будет вызывать много, вызов кэшированного делегата, ссылающегося на этот метод, примерно так же быстр, как и вызов самого метода. Однако, если вам нужно определить, какой метод вызывать так же, как вы собираетесь его вызывать, создание делегата для него очень дорого.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...