Programming
.NET
C#
May 2012 22

Погружаемся в глубины C# dynamic

Одним из наиболее заметных дополнений в C# 4 является dynamic. Об этом рассказано много и не раз. Но всегда выпускается из виду DLR (Dynamic Language Runtime). В данной статье мы рассмотрим внутреннее устройство DLR, работу самого компилятора, а также дадим определение понятиям статически-, динамически- типизированный язык со слабой и сильной типизациями. И, конечно же, не останется без внимания техника PIC (Polymorphic Inline Cache), используемая, например, в Google V8 engine.

Перед тем как двигаться дальше, хотелось бы освежить некоторые термины и понятия.

Чтобы не повторятся, под выражением переменная мы будем подразумевать любой объект данных (переменная, константа, выражение).

Языки программирования по критерию проверки типов обычно делятся на статически типизированные (переменная связывается с типом в момент объявления и тип не может быть изменен позже) и динамически типизированные (переменная связывается с типом в момент присваивания значения и тип не может быть изменен позже).

C# является примером языка со статической типизацией, в то время как Python и Ruby — динамически.

По критерию политики безопасности типов выделяют языки со слабой (переменная не имеет строго определенный тип) и сильной/строгой (переменная имеет строго определенный тип, который не может быть изменен позже) типизацией.

C# 4 dynamic keyword

Хоть dynamic и добавляет возможность написания чистого кода и взаимодействия с динамическими языками вроде IronPython и IronRuby, C# не перестает быть статически типизированным языком с сильной типизацией.

Перед детальным рассмотрением механизма самого dynamic, приведем пример кода:

//присваиваем первоначальное значение типа System.String
dynamic d = "stringValue";
Console.WriteLine(d.GetType());

//во время выполнения исключение не будет вызвано
d = d + "otherString";

Console.WriteLine(d);

//присваиваем значение типа System.Int32
d = 100;
Console.WriteLine(d.GetType());
Console.WriteLine(d);

//во время выполнения исключение не будет вызвано
d++;

Console.WriteLine(d);

d = "stringAgain";

//во время выполнения будет вызвано исключение 
d++;

Console.WriteLine(d);

Результат выполнения представлен ниже на скриншоте:



И что же мы видим? Какая же здесь типизация?

Отвечу сразу: типизация сильная, и вот почему.

В отличие от других встроенных типов языка C# (например, string, int, object и т.п.), dynamic не имеет прямого сопоставления ни с одним из базовых типов BCL. Вместо этого, dynamic – специальный псевдоним для System.Object с дополнительными метаданными, необходимыми для правильного позднего связывания.

Так, код вида:

dynamic d = 100;
d++;

Будет преобразован к виду:

object d = 100;
    object arg = d;
    if (Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee == null)
    {
        Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee = CallSite<Func<CallSite, object, object>>.Create(Binder.UnaryOperation(CSharpBinderFlags.None, ExpressionType.Increment, typeof(Program), new CSharpArgumentInfo[]
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
        }));
    }
    d = Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee.Target(Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee, arg);

Как видно, объявляется переменная d типа object. Далее в дело вступают binders из состава библиотеки Microsoft.CSharp.

DLR

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

Так, для кода вида

dynamic d = 100;
d++;

будет сгенерирован класс такого вида:

private static class <dynamicMethod>o__SiteContainerd
{
    // Fields
    public static CallSite<Func<CallSite, object, object>> <>p__Sitee;
}

Типом поля <>p__Sitee является класс System.Runtime.CompilerServices.CallSite. Рассмотрим его поподробнее.

public sealed class CallSite<T> : CallSite where T : class
{
    public T Target;
    public T Update { get; }
    public static CallSite<T> Create(CallSiteBinder binder);
}

Хотя поле Target и является обобщенным, на самом деле всегда является делегатом. И последняя строка в вышеприведенном примере не просто вариация операции:

d = Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee.Target(Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee, arg);

Статичный метод Create класса CallSite представляет собой:

public static CallSite<T> Create(CallSiteBinder binder)
{
    if (!typeof(T).IsSubclassOf(typeof(MulticastDelegate)))
    {
        throw Error.TypeMustBeDerivedFromSystemDelegate();
    }
    return new CallSite<T>(binder);
}

Поле Target является L0-кэшем (существуют также и L1-, и L2-кэши), которое используется для быстрой диспетчеризации вызовов на основе истории вызовов.

Обращаю внимание, что узел вызова является «самообучающимся», поэтому DLR необходимо периодически обновлять значение Target.

Для описания логики работы DLR приведу ответ Эрика Липперта по этому поводу (вольный перевод):

Сначала среда выполнения решает с объектом какого типа мы имеем дело (COM, POCO).

Далее в дело вступает компилятор. Так как необходимость в лексере и парсере отсутствует, DLR использует специальную версию компилятора C#, имеющего только анализатор метаданных, семантический анализатор выражений, а также генератор кода, который вместо IL генерирует Expression Trees.

Анализатор метаданных использует рефлексию, чтобы установить тип объекта, который потом передается семантическому анализатору для установления возможности вызова метода или выполнения операции. Далее происходит построение Expression Tree, как если бы Вы использовали лямбда-выражение.

Компилятор C# возвращает обратно дерево выражений в DLR вместе с политикой кэширования. DLR потом сохраняет данный делегат в кэше, ассоциирующимся с узлом вызовов.

Для этого используется свойство Update класса CallSite. При вызове динамической операции, хранящейся в поле Target происходит перенаправление к свойству Update, где и происходит вызов байндеров. Когда в следующий раз произойдет вызов, вместо повторного выполнения вышеперечисленных действий, будет использоваться уже готовый делегат.

Polymorphic Inline Cache

Производительность динамических языков страдает из-за дополнительных проверок и поисковых запросов, выполняющихся во всех местах вызова. Прямолинейная реализация постоянно ищет члены в списках приоритета класса и, возможно, разрешает перегрузку типов аргументов метода каждый раз, когда исполняется строчка кода. В языках со статической типизацией (или с достаточным количеством указаний типов в коде и выводом типов) можно генерировать инструкции или вызовы функций среды исполнения, которые подходят для всех точек вызова. Это возможно, потому что статические типы позволяют узнать все, что нужно, во время компиляции.

На практике повторные операции над одними и теми же типами объектов можно привести к общему типу. Например, при первом вычислении значения выражения x + y для целых x и y можно запомнить фрагмент кода или точную функцию времени исполнения, которая складывает два целых числа. Тогда при всех последующих вычислениях значения этого выражения искать функцию или фрагмент кода уже не потребуется благодаря кэшу.

Вышеприведенный механизм кэширования делегатов (в данном случае), когда узел вызова является самообучающимся и обновляющимся называется Polymorphic Inline Cache. Почему?

Polymorphic. Target узла вызова может иметь несколько форм, исходя из типов объектов, используемых в динамической операции.

Inline. Жизненный цикл экземпляра класса CallSite проходит именно в месте самого вызова.

Cache. Работа основана на различных уровнях кэша (L0, L1, L3).

+53
54.7k 287
Comments 26