Почему выражения с динамическими типами данных медленнее чем со статическими в C#?

Posted by VladymyrL on Sunday, November 18, 2018

Динамические типы данных добавляют гибкости нашему коду, они позволяют использовать скриптовые языки программирования в среде dotnet, позволяют хитрым образом избавится от написания лишнего кода при работы с объектами через рефлексию и вобще мегакруты. Но с большой силой приходит большая ответственность, а за лишнее удобство приходится платить дополнительную цену. И цена здесь - это скорость работы. Об этом должен знать каждый разработчик платформы дотнет. Но вот из-за чего происходит замедление, и насколько сильно просаживается скорость работы это уже вопросы со звёздочкой. И если вам так-же интересно знать ответы на эти вопросы, прошу проследовать под кат.

От переводчика: в этой статье я скомпоновал пару прекрассных ответов со стековерфлоу. По работе компилятора при взаимодействии с dynamic читать ответ Eric Lippert здесь.

Почему dynamic медленный?

При обращении к динамическим типам компилятор добавляет код, который добавляет создание объекта сайта (места) динамического вызова, внутри которого и происходит операция. К примеру, у вас есть вот такой кусок кода:

class Program
{
    class MyClass
    {
        public int GetY()
        {
            return 42;
        }
    }

    static void Main(string[] args)
    {
        dynamic y = new MyClass();
        var result = y.GetY();
    }
}

То компилятор выдаст код близкий к этому. (Реально код чуть сложнее, но нам этого хватит для демонстративных целей)

internal class Program
{ 
    private static void Main(string[] args)
    {
        if (a > b) d = c;
        object y = (object) new Program.MyClass();
        if (Program.<>o__1.<>p__0 == null)
        {
            Program.<>o__1.<>p__0 = CallSite<Func<CallSite, object, object>>.Create(
                Binder.InvokeMember(
                    CSharpBinderFlags.None, 
                    "GetY", 
                    (IEnumerable<Type>) null, 
                    typeof (Program), 
                    (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[]
                    {
                        CSharpArgumentInfo.Create(
                            CSharpArgumentInfoFlags.None, 
                            (string) null
                        )
                    })
                );
        }
    
    object result = Program.<>o__1.<>p__0.Target((CallSite) Program.<>o__1.<>p__0, y);
}

Давайте немного разберём, что же здесь проимходит? Во первых создаём обект-сайт вызова и кэшируем его, для использования в дальнейшем. Вызывающий объект-сайт будет существовать постоянно после того, как вы первый разобратитесь к нему. Объект-сайт вызова - это такой объект, в задачей которого является динамическое обращение к методу GetY.

После создания объекта сайта вызова произойдёт обычный вызов метода ?

Конечно нет! Этот объект представляет из себя времени run-time динамического языка (DLR). DLR как-бы дмает: «Хм, сейчас происходит обращение к динамическому методу foo вот такого объекта. Каким образом это должно произойти? Нет. Тогда я лучше узнаю.

После чего DLR просмотривает объект d1, чтобы разобраться, чем эта штука является. Это может быть COM-объект или объект Iron Python, или объект Iron Ruby, или объект IE DOM. Если это не кто-либо из них, тогда получается это не что иное как объект C#.

И вот тут снова дело в свои руки берёт компилятор. Здесь не нужен лексере или синтаксический анализатор, поэтому DLR вызывает особую версию компилятора C#, которая при помощи анализатора метаданных и семантического анализатора выражений возвращает деревья выражений, а не обычный IL.

Анализатор метаданных используя рефлексию определяет тип объекта d1, после чего передаст его семантическому анализатору, чтобы определить, что произойдёт, если у такого объекта вызываеть метод Foo. Анализатор перегрузки методов показывает эту информацию, а затем компилятор создаст дерево выражений - точно так же, как если бы вы вызвали этот метод Foo при помощи дерева выражений внутри lambda выражения.

На следующем этапе компилятор C# передаст, созданное дерево выражений назад в DLR дополнив политикой кэширования. Обычно эта политика представляет из себя правило - во второй раз, когда вы увидите объект такого типа, используйте уже созданное дерево выражений, а не обращаться ко мне. После чего DLR осуществляет компиляцию дерева выражений при помощи в свою очередь компиляторп expression-tree-to-IL и возвращает блок IL как делегат.

DLR кэширует созданный делегат в кеше, связанном с объектом сайта вызова.

Затем он вызывает делегат, и происходит вызов Foo. В следующие разы, когда вы вызываете в M, у нас уже есть возможность обратится к сайт-обьекту. DLR снова запросит объект, и если объект такого-же типа, что и в прошлый раз, он извлечёт делегат из кэша и вызовет его. Но в случае если прийдёт объект другого типа, то кеш промахётся, и весь процесс придётся пройти заново.

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

int x = d1.Foo() + d2;

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

Ну как? Уже не кажется такой хорошей идеей завести больше обращений к dynamic? С другой стороны представте как много кода не пришлось писать вам. А ведь лучший код, это не написанный код!

На сколько же плохо для производительности лишнее обращение к dynamic?

Судя по бэнчмарку использование dynamic просаживает скорость в 6.7 раз по сравнению с статически типизированным кодом. Насколько это подходит для вашего случая, можно посудить взглянув на сравнительную таблицу и прикинув альтернативные решения опирающиеся на другие типы вызова:

Тип вызова Относительное время вызова
Обычный метод 1
Метод расширения 1,19
Виртуальный интерфейсный метод 1,46
Обобщённый метод 1,54
DynamicMethod.Emit 2,07
MethodInfo.CreateDelegate 2,13
Visitor Accept/Visit 2,64
Linq выражение 5,55
Ключевое слово dynamic 6,70
MethodInfo Invoke 102,96

Если вдруг интересны детали исследования просадки производительности и вы хотите перепроверить коэфициенты вы можете найти исследование здесь.


comments powered by Disqus