Погружаемся в глубины 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).
    Share post

    Similar posts

    Comments 26

      +1
      Хорошая статья, спасибо! Всегда полезно знать внутреннее устройство используемых механизмов
        +1
        Однако…
          0
          Честно говоря до этого думал, что DLR объект ничто иное как таблица
            +1
            Неделя C# продолжается.
              +3
              А за статью спасибо. Интересное чтиво.
                +2
                Смешно про неделю получилось
                Я флеш-разработчик, но давно поглядывал на C# — пробовал смотреть видеоуроки, а в начале этой недели получил 2 заказанные книги по C# — решил учить его. Так что, надеюсь, началась не неделя, а что-то более длинное :)
                +1
                Очень познавательно, спасибо!
                  +2
                  Спасибо, за статью.
                  Только не совсем понял про Target и L0 кеш. В виртуальной машине специально прописана оптимизация для CallSite, что Target всегда должен лежать в регистре? Ведь CallSite это по сути класс и место ему в куче, а адресация как у всех идёт через смещение.
                    0
                    Если что я имею в виду JIT
                      +2
                      не совсем так )
                      L0 — это не кэш процессора. L0-2 кэши являются абстракциями в рамках архитектуры DLR. CLR понятия не имеет о CallSite как таковом. В отличие от нового ThreadPool в CLR 4 необходимого для TPL, DLR не затрагивает изменения в виртуальной машине.

                      Насчет JIT: конструкция вида

                      dynamic a = 2;
                      dynamic b = 3;
                      dynamic c = a + b;
                      

                      при генерации делегата динамического узла практически будет эквивалентна статическому коду C#, т.е. будет создано лишь несколько машинных инструкций.

                      p.s.
                      еще кое-что о кэшах DLR )
                      если L0 — это непосредственно динамический метод, то
                      L1 — совокупность последних 10 использованных динамических методов, т.е. при нахождении нового типа объектов, вместо генерации нового динамического метода и обновления Update, DLR ищет эквивалентный метод в L1.
                      L2 — т.к. каждому динамическому узлу требуется 1 делегат и 1 экземпляр конкретного байндера, то L2 хранит делегаты с обобщенным экземпляром байндера. на данный момент L2 может хранить до 100 делегатов.
                        +1
                        Спасибо, теперь стало понятно.
                      0
                      Я так понимаю, это некий аналог JIT-компилятора, только применительно к динамической типизации? А отличие лишь в том, что JIT-компилятор компилирует вызовы единожды (и если уже скомпилирован, вызывает напрямую), а в рассматриваемом DLR polymorphic inline cache собирает вызываемый кусок кода только в случае промаха кеша (а в противном случае дергает напрямую делегат).
                      Неясно только, как можно вообще закешировать такую штуку, как операция над объектом неизвестного типа. Что является «ключом» кеша? Комбинация {реальный тип объекта; выражение, которое необходимо к нему применить}?
                        +1
                        >>Я так понимаю, это некий аналог JIT-компилятора, только применительно к динамической типизации?

                        образно — да.

                        >>Неясно только, как можно вообще закешировать такую штуку, как операция над объектом неизвестного типа. Что является «ключом» кеша?

                        «ключом» является сигнатура вызовов и сам байндер с метаданными.
                        например для:

                        dynamic x = 2;
                        dynamic y = 3;
                        
                        int c = x + y;
                        
                        long d = x + y;
                        


                        будет создан полиморфный делегат с таким кодом:

                        object GeneratedMethod(object x, object y) {
                        if (x is int && x is int)
                        return (object) ((int)x + (int)y);
                        if (x is long)
                        return (object) ((long)x + (long)y);
                        }
                        0
                        Здравствуйте.
                        У меня к вам вопрос по поводу dynamic + .NET Remoting.

                        Допустим есть интерфэйс IRemoteCom и его метод DoSomething()
                        на сервере есть реализация IRemoteCom и этого метода.
                        Обычное использование:
                        var rObj = (IRemoteCom)Activator.GetObject(typeof(IRemoteCom),"tcp://localhost:1002/Test");
                        rObj.DoSomething();
                        

                        работает ожидаемо.

                        Но когда я хочу использовать dynamic,
                        dynamic dObj = (IRemoteCom)Activator.GetObject(typeof(IRemoteCom),"tcp://localhost:1002/Test");
                        dObj.DoSomething();
                        

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

                        Теперь вопрос: существует ли возможность научить dynamic вызывать методы через прокси? И как это сделать.

                        Спасибо.
                          0
                          Здравствуйте!

                          >>существует ли возможность научить dynamic вызывать методы через прокси? И как это сделать.

                          Как вариант можно использовать reflection. для этого необходимо создать динамический прокси-класс.
                          Например:

                          public class DynamicProxy : System.Dynamic.DynamicObject
                          {
                              private readonly object _instance;
                          
                              public DynamicProxy(object instance)
                              {
                                  _instance = instance;
                              }
                          
                              public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
                              {
                                  if (binder.Name == "DoSomething")
                                  {
                                      var objType = _instance.GetType();
                          
                                      var remoteInterface = objType.GetInterface("IRemoteCom");
                                      var method = remoteInterface.GetMethod("DoSomething");
                                      result = method.Invoke(_instance, args);
                          
                                      return true;
                                  }
                          
                                  result = null;
                                  return false;
                              }
                          }
                          

                          используя binder.Name можно проверять или нет список разрешенных объектов, а можно и вообще как параметр для remoteInterface.GetMethod(binder.Name).

                          надеюсь это поможет решить проблему.
                            0
                            :)
                            Спасибо. Я находил это решение на StackOverflow.
                            Моя проблемма в том, что я не знаю интерфейса (ни типа, ни имени).
                            Существует ли решение в этом случае?
                              0
                              тогда нет, т.к. при отсутствии метаданных DLR не сможет сгенерировать ничего.
                                0
                                а вытащить из прокси эти данные невозможно?
                                что нибудь типа GetRealType?
                                  0
                                  с таким не встречался, но наверное Вам стоит посмотреть в сторону WCF?
                                    +1
                                    Попробую, мы как раз на .NET4 перешли.
                                    Спасибо за советы.
                          0
                          Пишу интерпритатор своего языка на C#… Сначала юзал просто Object'ы, прочел статью, вспомнил про dynamic, переписал все под него, но вот проблема: все начало работать очень медленно… Решил продолжить юзать Object'ы (:
                            0
                            весьма странно — вы используете reflection или code emitter вместо dynamic? просто используя pic падение производительности ~15% по сравнению с типизированным кодом.
                              0
                              Немного не понял, что вы имеете ввиду, не мастер в этом… Когда я здесь:
                              	public class Tocken
                              	{
                              		public TockenType Type { get; private set; }
                              		public object Value { get; private set; }
                              		
                              		public Tocken (object Value, TockenType Type)
                              		{
                              			this.Value = Value;
                              			this.Type = Type;
                              		}
                              	}

                              И здесь:
                              	public class Node
                              	{
                              		public NodeKind Kind;
                              		public Object Value;
                              		public List<Node> Nodes;
                              
                              		public Node (NodeKind Kind, Node Value,  List<Node> Nodes)
                              		{
                              			this.Kind = Kind;
                              			this.Value = Value;
                              			this.Nodes = Nodes;
                              		}
                              		
                              		public Node (NodeKind Kind, Object Value)
                              		{
                              			this.Kind = Kind;
                              			this.Value = Value;
                              			this.Nodes = new List<Node>();
                              		}
                              
                              		public void Write (int depth)
                              		{
                              			for (int i = 0; i < depth; i++)
                              				Console.Write ('|');
                              			Console.Write ('+' + Kind.ToString ().ToUpper ());
                              
                              			if (Value != null)
                              			if (Value.GetType () == typeof(Node)) {
                              				Console.WriteLine();
                              				(Value as Node).Write (depth + 1);
                              			} else {
                              				if (Value != null)
                              					Console.Write("={1}", Kind.ToString ().ToUpper (), Value.ToString ());
                              			}
                              			foreach (Node n in Nodes) {
                              				Console.WriteLine();
                              				n.Write(depth + 1);
                              			}
                              
                              			if(depth == 0)
                              				Console.WriteLine();
                              		}
                              	}

                              Менял Object на dynamic, синтаксическое дерево этого кода:
                              def a = 5
                              строилось около 3 секунд вместо где-то четверти секунды…

                              P.S. Компилится под моно
                                0
                                Во-первых, 2 ошибки в написании слов — это перебор (интерпретация и token).

                                Во-вторых, возможно, JIT еще не прошелся с DLR.

                                В-третьих, (Value as Node), где Value — dynamic — будет вызван метод TryConvert (если это DynamicObject), если нет то отдельный call site будет обслуживать это дело, т.е. MSIL инструкция isinst не будет задействована.

                                Также при создании объектов Mono проигрывает CLR на ~50%.
                                  +1
                                  Ну с token'ом — это привычка :| А интерпретация, сам не знаю…
                                  Спасибо за ответ…
                                    0
                                    думаю, лучше пройтись профайлером, т.к. результаты могут зависеть еще и от входных данных.

                          Only users with full accounts can post comments. Log in, please.