среда, 10 сентября 2008 г.

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();
}

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

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);
}

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

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));
}

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

screenshot002

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

Опубликовать

2 комментария:

  1. Гы, мы какараз этот солюшен заюзали, я какраз планировал писать про это пост ;), хорошо что наткнулся на ваш ;). Теперь будет меньше писать.

    ОтветитьУдалить
  2. Не работает на Guid, DateTime

    ОтветитьУдалить