Expression Trees и оптимизация Reflection

    В последней версии .NET Framework среди новых возможностей было добавлено средства метапрограммирования под названием Expression Trees. На базе этой технологии, а именно основываясь на том принципе, что выражения на "обычном" языке программирования могут автоматически преобразовываться в синтаксические деревья, была разработана технология LINQ.

    Но в этом посте речь пойдет о другой области применения возможности динамически собирать expression trees и компилировать их в работоспособный код. И эта область — оптимизация Reflection.

    кросс-пост с персонального блога


    Как известно, платой за гибкость при использовании рефлексии является производительность. Но в случае, когда она применяется к некому фиксированному набору метаданных, ее легко оптимизировать.

    Но динамические Expression Trees предоставляют нам еще достаточно элегантный способ оптимизации. Суть его заключается в том, что для доступа к свойствам экземпляров известного класса мы будем генерировать соответствующий строго типизированный код в виде лямбда-функции, которая будет обращаться к ним напрямую и которую мы будем кэшировать для последующего повторного использования.

    Сам по себе метод создания нужной лямбда-функции достаточно прост:

    private static Func<object, object> CreateGetter(object entity, string propertyName) 

      var param = Expression.Parameter(typeof (object), «e»); 
      Expression body = Expression.PropertyOrField(Expression.TypeAs(param, entity.GetType()), propertyName); 
      var getterExpression = Expression.Lambda<Func<object, object>>(body, param); 
      return getterExpression.Compile(); 
    }
    * This source code was highlighted with Source Code Highlighter.


    Если поставить в этом методе точку останова и посмотреть на строковое представление переменной getterExpression, то мы увидим, во что оно будет скомпилировано:



    Обернем всю логику доступа к свойству класса в некий ReflectionHelper, который в дальнейшем можно будет расширить методами для вызова методов, инициализации свойств и т.д. Этот класс будет реализовывать метод GetPropertyValue следующим образом:

    readonly Dictionary<PropertyGetterKey, Func<object, object>> propertyGetters = new Dictionary<PropertyGetterKey, Func<object, object>>();

    public object GetPropertyValue(object entity, string propertyName)
    {
      Func<object, object> getter;

      var key = new PropertyGetterKey {Type = entity.GetType(), PropertyName = propertyName};

      if (propertyGetters.ContainsKey(key))
        getter = propertyGetters[key];
      else
      {
        getter = CreateGetter(entity, propertyName);
        propertyGetters.Add(key, getter);
      }

      return getter(entity);
    }
    * This source code was highlighted with Source Code Highlighter.


    Для проверки того, насколько эта логика эффективна, разработаем небольшой тест:

    var entities = new List<Class1>();

    for (var i = 0; i < 20; i++)
      entities.Add(new Class1 { Property1 = «Value» + i });

    foreach (var entity in entities)
    {
      var start = DateTime.Now.Millisecond;
      var val = ReflectionHelper.Instance.GetPropertyValue(entity, «Property1»);
      Console.WriteLine("{0} — {1}", val, (DateTime.Now.Millisecond — start));
    }
    * This source code was highlighted with Source Code Highlighter.


    Ну и результаты говорят сами за себя:



    Как видим, такой способ оптимизации более чем жизнеспособен :)

    Комментарии 29

      +2
      Думаю было б неплохо перенести в блог про .NET
        +1
        Сейчас попрошусь к ним :)
        +2
        я пишу на .net, но мне в голову не приходит то, где я могу использовать эти техники
        можете рассказать область применения или может быть примеры какие привести, для этого даже отдельную статью можно было бы сделать
          +2
          Могу отдать тему на дальнейшую разработку :)
          А область применения этой техники такая же, как и у рефлексии, в общем-то…

          Например, задачка валидации состояния объектов на основе информации из какого-то файла конфигурации или даже DSL. Или самый «подножный» пример — это разработка своего супер-универсального DAL, чтобы одним методом проинициализировать любой тип данными из БД…
            0
            Ждем подробных постов :)
              +1
              Да, так и есть. Писал когда-то свой класс, который мапится на таблицы в соответствии с аттрибутами свойств. Класс принимал в качестве параметра конструктора DataReader и из него инициализировался. Было бы полезно там оптимизировать, да только уже не поддерживаю этот проект :)

              А как бы это сравнить с Reflection? намного ли быстрее работает, если суммарно взять результат по вашей коллекции из 20 элементов. Только тут, мне кажется, нужно будет графики составить некоторые, так как при маленьком объеме кол-ве данных будет быстрее рефлексия, а при большем кол-ве — ваш вариант. Хотя… Говорю навскидку, могу сильно ошибаться, время уже позднее ) Просто 23ms для получения _одного_ свойства из _одного_ объекта (как при первом вызове) — это достаточно медленно.
                0
                Здесь хорошая статья о том, как там дела обстоят со скоростями в Rеflection и различных способах оптимизации
                  0
                  Спасибо, ещё по ссылке в топике почитал. Только там используются возможности фреймворков более ранних версий (результатов с LINQ и вашего метода там нет)
                    0
                    Навскидку.

                    Такой код:

                    foreach (var entity in entities)
                    {
                       var stopWatch = new Stopwatch();
                       stopWatch.Start();

                       var pi = typeof(Class1).GetProperty(«Property1», BindingFlags.Instance | BindingFlags.Public);
                       var val = pi.GetValue(entity, null);

                       stopWatch.Stop();
                       Console.WriteLine(«{0}\t{1}», val, stopWatch.Elapsed);
                    }/* This source code was highlighted with Source Code Highlighter.*/


                    Показывает такой результат:



                    Что дает повод крепко пофлудить :)
                      0
                      Да, ещё и какой. Но не будем. :)

                      Я считаю, что каждый сам посчитать может. Ваш метод будет эффективен только при ОЧЕНЬ большом кол-ве записей. Иначе время, потраченное на первоначальную компиляцию не оправдает себя. Я это как-то так вижу.
            0
            Вот убей не пойму зачем сразу оптимизировать. Возможно, и не тормозит ничего. Контекст решает. Тема безусловно хорошая, автору плюс. Конечно, запомню, возможно, буду применять.
            А вот про разбор Expression Tree было бы намного интереснее побеседовать. Уж больно популярны стали «Fluent» фреймворки. Может автор возьмется «разобрать» тему?;)
              0
              Безусловно, преждевременная оптимизация — зло. Но зато если она «навалилась» — для этого и написан этот пост :)
              А про разбор expression trees придется дождаться подходящей проблемы в текущем проекте :)
              +3
              Да, и ещё обратил внимание, что в тесте используется DateTime.Now.Millisecond — start!!!
              Как-то, нули ничего не говорят о порядках ускорения. Может все-таки StopWatch использовать? Привыкли уже к нему вроде.
                0
                Специально для вас :)

                  0
                  Ну, почему только для меня?:) Я не жадный, пусть все смотрят. Но чистота эксперимента… и все такое.
                    0
                    Да, совет дельный :)
                0
                Вы знаете, после этого открываешь руби и видишь будущее С# и прошлое Lisp & SmallTalk :-)
                  +1
                  Есть такая тенденция… Всякая функциональщина и динамика активно приходят в мейнстрим.

                  Но если и сравнивать это с каким-то языком, то скорее с Nemerle, который все эти штуки уже давно именно в .NET превнес.
                    0
                    Ну а кто это принес в сам Nemerle? :) Уж синтаксические деревья, Expression Trees и их генерация — это чистой воды lisp, который был придуман лет 50 назад. А после него это было реализованно мо многих других языках, как чисто в функциональных, так и в объектных. Что касается c#, то пока мы может посмотреть на примере, как __«что-то подобное»__ можно сделать на этом языке прибигая вот к таким методам. Ну а вообще ты прав, динамика активно приходит в мейнстримы.
                      0
                      Ну дык о роли LISP и SmalTalk никто и не спорит :) Nemerle был приведен, как более близкий для .NET язык, чем Ruby :)

                      А насчет C# — да, при всех своих новых достоинствах он остается слишком уж объектно-ориентированым…
                        0
                        >>Nemerle был приведен, как более близкий для .NET язык, чем Ruby :)

                        Дык я не говорил о наиболее технологической «близости», а скорее об глобально-идейной… наверное правильно было бы указать и другие языки, но что делать если в текущий момент я изучаю Руби? :)
                          0
                          Говорю сразу — я ничего против Ruby не имею :)
                        0
                        А какая разница, кто что куда привнёс?
                        Всё уже украдено до нас © (Ы)
                          0
                          Большая разница, так как знание корней и причин появление вещей не только полезно, но еще и интересно. Ну это к теме разговара не относится.
                    0
                    В этом решении есть проблемы. Работа такого геттера зависит от типа Property у класса. Например, для свойства с типом Double такое дерево вообще не компилируется. Выскакивает исключение об невозможности делегирования типа Double к типу Object
                      0
                      Коллега )
                      Тоже нашел по поиску эту статью (ей уже почти полтора года) и тоже споткнулся о значимый тип!
                      Вы, случаем, не нашли как обойти эту проблему?
                        0
                        Всё, понял, просто тема новая). Надо поменять сигнатуру функции на нужную, в вашем случае: Func<object, double>
                          0
                          Да, Func<object, double> в данной ситуации помогает, но теряется универсальность.
                          Есть решение: использовать обобщения (Generics) — но это опять-таки не всегда даст нужный результат.

                          А статья хоть и довольно не свежая, но содержит вполне актуальные данные. Это поистине приятное нововведение в C# 3.
                            0
                            Если в контексте Вашей программы можно использовать Func<object, double>, т.е. нет полной универсальности, то можно и так.
                            Если же хотите возвращать и значимые типы в качестве результата, то можно дерево немного дополнить
                            (t as Class).Property следующим образом: ((t as Class).Property as object)

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое