.NET
26 октября 2009

DataBinder и скорость

Во многих проектах есть необходимость доступа к свойствам объектов используя механизм System.Web.UI.DataBinder.Eval и встал вопрос: насколько это быстро работает и можно ли выполнять такую операцию еще быстрее?

Итак, был сделан небольшой тест. Для начала сделали 2 класса (для тестирования виртуальных и переопределенных свойств), к свойствам которых будем обращаться:
  public class c1
  {
    string _str;
    public c1(string str)
    {
      _str        = str;
    }
    public string Str1     { get { return "1: "+_str; } }
    public virtual string Str2 { get { return "2: "+_str; } }
  }

  public class c2 : c1
  {
    public c2(string str)
      : base(str)
    {
    }
    public override string Str2 { get { return "C"+base.Str2; } }
  }

Теперь собственно тест:
c2 c        = new c2("str");

// Прямой доступ к свойствам
for (int i=0; i<ITERATIONS; i++)
{
  String.Format("Str1: {0}, Str2: {1}", c.Str1, c.Str2);
}
// Доступ через DataBinder
for (int i=0; i<ITERATIONS; i++)
{
  String.Format("Str1: {0}, Str2: {1}", DataBinder.Eval(c, "Str1"), DataBinder.Eval(c, "Str2"));
}

Результат:
  • Прямой вызов: 00:00:00.065 (100%)
  • DataBinder.Eval: 00:00:01.135 (1746%)

Ничего себе, в 17 раз медленнее! Хотя что-то похожее и предполагалось.
Для увеличения скорости сначала было предложено кэшировать обращения к Reflection (куда же без него в такой задаче), но серьезного ускорения это не дало. Тогда был опробован подход с генерацией кода обращения на лету, то есть:
  1. При первом обращении изучаем класс и свойство через Reflection
  2. Идем к тому классу, где свойство было определено или переопределено
  3. Строим класс, который состоит из одного метода: получить значение нужного свойства у объекта определенного типа
  4. Вызываем построенный метод

Код генерации приводить в топике не буду, кому интересно скачивайте Проект (29Кб).
c2 c        = new c2("str");
PropertyAccessor p1  = PEval.GetPropertyAccessor(c.GetType(), "Str1");
PropertyAccessor p2  = PEval.GetPropertyAccessor(c.GetType(), "Str2");
for (int i=0; i<ITERATIONS; i++)
{
  String.Format("Str1: {0}, Str2: {1}", p1.GetValue( c ), p2.GetValue( c ));
}

Результат выполнения:
  • Кэшированный вызов: 00:00:00.065 (100%)

Отличный результат! По скорости сопоставим с прямым вызовом, при нескольких запусках это время иногда было меньше 100%. Вызов назвал кэшированным так как класс с методом был запомнен и шло постоянное обращение без проверки существования такого класса.
Для завершения эксперимента был написан статический класс, который кэшировал построенные классы и создавал их по мере надобности.
c2 c        = new c2("str");
for (int i=0; i<ITERATIONS; i++)
{
  String.Format("Str1: {0}, Str2: {1}", PEval.GetValue(c, "Str1"), PEval.GetValue(c, "Str2"));
}

Результат выполнения:
  • PEval.GetValue: 00:00:00.090 (138%)

Всего на 38% медленнее прямого вызова. Несколько запусков дали средний результат 150%, но все равно это не 1746%.
Полный вывод теста:
Create PropertyAccessor for TestDataBinding.c1.Str1
Create PropertyAccessor for TestDataBinding.c2.Str2
=========================
ITERATIONS: 100000
Прямой вызов: 00:00:00.065 (100%)
PEval.GetValue: 00:00:00.090 (138%)
Кэшированный вызов: 00:00:00.065 (100%)
DataBinder.Eval: 00:00:01.135 (1746%)
=========================
TestDataBinding.c2 (TestDataBinding.PropEvaluator.PropertyAccessor) [200003]
TestDataBinding.c1_Str1_Accessor => Str1 (G) [100002]
TestDataBinding.c2_Str2_Accessor => Str2 (G) [100001]
TestDataBinding.c1 (TestDataBinding.PropEvaluator.PropertyAccessor) [2]
TestDataBinding.c1_Str1_Accessor => Str1 (G) [100002]
=========================


2 миллиона вызовов:
=========================
ITERATIONS: 1000000
Прямой вызов: 00:00:00.930 (100%)
PEval.GetValue: 00:00:01.085 (116%)
Кэшированный вызов: 00:00:00.738 (79%)
DataBinder.Eval: 00:00:10.976 (1180%)
=========================


И еще в 10 раз больше:
=========================
ITERATIONS: 10000000
Прямой вызов: 00:00:06.802 (100%)
PEval.GetValue: 00:00:10.917 (160%)
Кэшированный вызов: 00:00:07.017 (103%)
DataBinder.Eval: 00:01:45.476 (1550%)
=========================


Что еще можно добавить:
  1. Сделать методы для изменения значения свойства
  2. Расширить класс доступа к свойству дополнительными функциями: отображаемое имя свойства, форматирование значения в строку/парсинг значения из строки, валидация значения
  3. Сделать вызов не одного свойства, а цепочки PEval.GetValue(o, "Prop1.Prop2.Prop4")


Дополнение 1.
Диаграмма классов более продвинутой версии:
Диаграмма классов для доступа к свойствам

Диаграмма классов генератора кода:
image
+15
975 14
Комментарии 15