Сообщений 14    Оценка 426        Оценить  
Система Orphus

Скорость Reflection .Net

Сравнение быстродействия различных способов получения данных из объектов в .Net

Автор: Михаил Полюдов
Источник: RSDN Magazine #5-2003
Опубликовано: 11.04.2004
Исправлено: 13.03.2005
Версия текста: 1.0
Введение
Описание тестов
Тестовый класс
Исходный код тестов
Циклы
Direct
Switch
Reflection
Оптимизация №1
Оптимизация №2
Оптимизация №3 «по Владу»
Оптимизация №4 «по Владу с изменениями от автора»
Reflection Direct («Влад, Андрей и другие»)
Reflection Indirect «Влад, Андрей и автор»
Результаты тестов
Таблица результатов
Графики
Итоги

Плавно, Ускоренно,
Быстро, Быстрее,
Быстро, как только можете,
Быстрее, Еще быстрее… Записки на нотах.

Код к статье

Введение

Перед программистом зачастую стоит задача получения информации из объектов «по имени», то есть получения информации через имя свойства, которое неизвестно заранее, либо задано каким-либо атрибутом. Примером может служить DataBinding, который активно используется визуальными control-ами из пространства имен System.Windows.Forms, входящего в состав .Net Framework.

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

Описание тестов

Для тестов было написано простейшее приложение с одной формой (проект – Windows Application). В качестве языка программирования выбран язык C#.

Для получения более-менее адекватных результатов использовалось четыре прохода, в которых производилось 1 000, 10 000, 100 000 и 1 000 000 итераций для каждого метода получения данных.

Все тесты производились на компьютере Pentium IV 1600 MHz, 512 MB RAM (DDR 266), Windows XP Corporate Final (Service Pack 1).

Тестовый класс

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

Атрибут:

[AttributeUsage(AttributeTargets.Property)]
class DispNameAttribute: Attribute
{
  private string _displayName;
  public string DisplayName
  {
    get { return _displayName; }
  }
  public DispNameAttribute(string DisplayName)
  {
    _displayName = DisplayName;
  }
}

Тестовый класс:

class TestClass
{
  [DispName("Целое")]
  public int IntValue
  {
    get { return 1;}
  }
  [DispName("Строка")]
  public string StringValue
  {
    get { return "Строка!"; }
  }

  [DispName("Дата")]
  public DateTime DateValue
  {
    get { return DateTime.Now.Date; }
  }
}

Итак, мы имеем класс, у которого есть 3 свойства. У этих свойств есть следующие параметры:

Имя свойстваТип свойстваОтображаемое имяЗначение
IntValueint (System.Int32)Целое1
StringValuestring (System.String) Строка«Строка!»
DateValueDateTime (System.DateTime)ДатаТекущая дата

Кроме того, мы будем использовать разнообразные методы доступа к свойствам. В случаях, когда мне нужно описать подобный способ доступа к какому-либо объекту, я обычно использую одну из наиболее интересных особенностей языка программирования C# – индексеры. Но в нашем случае имеется небольшая проблема – C# не позволяет (в отличие от Delphi) иметь именованные индексеры, а описание нескольких индексеров с одинаковой сигнатурой – это неправильно. Поэтому мы «обманем» C# и опишем несколько индексеров, у которых просто будем добавлять «лишние» параметры типа int, которые не будут нигде использоваться и, наверняка, будут оптимизированы компилятором (на месте компилятора я именно так и поступил бы). Данное добавление int’ов сделает сигнатуры индексеров разными и позволит иметь один тестовый класс вместо шести. Конечно, можно было использовать вместо индексеров методы, но мне индексеры нравятся больше :)

Итак, далее следуют описания способов доступа к свойствам:

Способ доступаОписаниеДоступ через отображае-мые имена
DirectПрямой доступ к свойствамНет
SwitchДоступ к значениям свойств через Switch по имени свойства, которое передается в индексер.Есть
ReflectionСамый «деревянный» способ. Осуществляется полный перебор всех свойств объекта, помеченных атрибутом, и проверяется, не совпадает ли имя свойства или значение свойства DisplayName атрибута с параметром индексера.Есть
Оптимизация №1При инициализации объекта создается хэш-таблица, ключами которой являются все имена свойств объекта, а также виртуальные (отображаемые) имена, заданные с помощью атрибута DispName. Значениями являются объекты PropertyInfo, описывающие данное свойство. При вызове индексера производится поиск в хэш-таблице, и через найденный объект PropertyInfo производится получение значения свойства. Есть
Оптимизация №2Как и в предыдущем случае, при инициализации объекта в хэш-таблицу вносятся все имена свойств, а в качестве значений используются целые числа. Для свойства, независимо от того, под каким (реальным или отображаемым) именем оно добавляется, используется одно и то же число. Для разных свойств используются разные числа.Есть
Оптимизация (Vlad)Данный способ был предложен Владиславом Чистяковым.Перед началом цикла создаётся по объекту-делегату для каждого свойства (в зависимости от типа свойства используется тот или иной тип делегата), в цикле обращения осуществляются не к свойству, а к делегату.Нет
Оптимизация
(Vlad + Hacker_Delphi)
Способ получения данных взят у предыдущего способа, но данные получаются через индексер, который, в свою очередь, обращается за делегатом к хэш-таблице.Есть
Reflection DirectВ цикле вызывается конструкцияObjType.GetProperty("<Имя свойства>").GetValue( obj, new object[0]); Reflection используется напрямую.Нет
Reflection Direct (Hacker_Delphi)Тот же способ, но оформленный как очередной индексер.Нет
ПРИМЕЧАНИЕ

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

Теперь – код тестов.

Исходный код тестов

Циклы

Все циклы, которые использованы в данном примере, выглядят абсолютно одинаково:

PerfCounter pc = new PerfCounter();
pc.Start();
for (int i = 0; i < _iterationCount; i++)
{
  // Здесь – обращение к данным
}
float FinishTime = pc.Finish();

PerfCounter – класс (если быть точным – структура), позволяющий производить точное измерение времени выполнения участков кода. Для измерения времени этот класс использует функции WinAPI QueryPerformanceCounter и QueryPerformanceFrequency. Его код можно найти на нашем сайте (http://www.rsdn.ru/forum/Message.aspx?mid=249579&only=1).

FinishTime – переменная, отвечающая за хранение времени выполнения одного конкретного теста. Соответственно, эти переменные имеют имена вида FinishXXX, где XXX – число от 1 до 9, по количеству тестов.

Direct

Для данного способа есть только код цикла:

// Алгоритм не может поддерживать работу с DisplayName'ами, так что...
int ti = t.IntValue;
string si = t.StringValue;
DateTime di = t.DateValue;
// ...просто повторим тест два раза
int ti2 = t.IntValue;
string si2 = t.StringValue;
DateTime di2 = t.DateValue;

С этим способом, в общем-то, все ясно. Данный тест выполняется исключительно для того, чтобы получить «эталонное» значение для сравнения с остальными способами получения данных.

Switch

Для этого теста используется индексер:

public object this[string pName]
{
  get
  {
    switch (pName)
    {
      case "Целое":
      case "IntValue":
      {
        return IntValue;
      }
      case "Строка":
      case "StringValue":
      {
        return StringValue;
      }
      case "Дата":
      case "DateValue":
      {
        return DateValue;
      }
    }
    return null;
  }
}

Данный код просто делает switch по имени свойства и возвращает значение соответствующего свойства.

ПРИМЕЧАНИЕ

В одной из ближайших моих статей будет описан метод получения таких свойств для уже готовых классов.

И, соответственно, другой тип цикла:

// Сначала - по реальным именам
int ti = (int)t["IntValue"];
string si = (string)t["StringValue"];
DateTime di = (DateTime)t["DateValue"];
// А теперь - по именам, которые заданы через атрибут
int ti2 = (int)t["Целое"];
string si2 = (string)t["Строка"];
DateTime di2 = (DateTime)t["Дата"];

Я думаю, без излишних комментариев понятно, что делает этот код.

ПРИМЕЧАНИЕ

Далее по тексту будет использоваться ссылка на этот код как на «цикл для свойств с отображаемыми именами».

Reflection

Тот самый «деревянный» способ:

public object this[string pName]
{
  get 
  {
    PropertyInfo []pis = ObjType.GetProperties();
    foreach (PropertyInfo pi in pis)
    {
      object []attrs = 
        pi.GetCustomAttributes(typeof(DispNameAttribute), true);
      if (attrs.Length == 0)
        continue;
      if (pi.Name == pName)
        return pi.GetValue(this, new object[0]);
      else 
        for (int i = 0; i < attrs.Length; i++) 
        {  
          if (((DispNameAttribute)attrs[i]).DisplayName == pName)
          {
            return pi.GetValue(this, new object[0]);
          }
        }
    }
    return null;
  }
}

Данный алгоритм просто перебирает все описания свойств и проверяет, не найдено ли нужное свойство.

ПРИМЕЧАНИЕ

Порядок проверки длины массива атрибутов и имени свойства – не ошибка… просто передо мной стояла задача получать значения именно «обатрибученых» свойств.

Предваряя график и таблицу результатов тестирования, скажу: НИКОГДА НЕ ИСПОЛЬЗУЙТЕ ЭТОТ СПОСОБ ДОСТУПА К СВОЙСТВАМ ПО ИМЕНАМ, ну кроме, разве что, случаев, когда вам нужно написать код быстро, а быстродействие вас не волнует.

Оптимизация №1

Объявление дополнительных полей:

Hashtable optimize1 = new Hashtable();
public Type ObjType = null;

Код, добавляемый в конструктор:

ObjType = GetType();
PropertyInfo []pis = ObjType.GetProperties();
foreach (PropertyInfo pi in pis)
{
  object []attrs = pi.GetCustomAttributes(typeof(DispNameAttribute), true);
  if (attrs.Length == 0)
    continue;
  optimize1.Add(pi.Name, pi);
  foreach (DispNameAttribute attr in attrs)
  {
    if (optimize1[attr.DisplayName] != pi) 
      optimize1.Add(attr.DisplayName, pi);
  }
}

Этот код сканирует описания всех свойств по тому же алгоритму, что и предыдущий, и заносит в хэш-таблицу объект PropertyInfo для свойств, которые удовлетворяют нашим условиям.

ПРЕДУПРЕЖДЕНИЕ

В данном цикле специально используется метод Add класса Hashtable, а не присвоение с помощью индексера. Это сделано для того, чтобы не было возможности создать в объекте несколько свойств с «конфликтующими» именами. Исключение – совпадение отображаемого и обычного имени.

Код индексера:

public object this[string pName]
{
  get 
  {
    PropertyInfo pi = (PropertyInfo)optimize1[pName];
    if (pi == null)
      return null;
    else
      return pi.GetValue(this, new object[0]);
  }
}

Данный код просто ищет в хэш-таблице соответствующий объект PropertyInfo и получает значение свойства.

Цикл – стандартный для отображаемых имен.

Оптимизация №2

Этот способ – некая комбинация предыдущего и switch-способов.

В конструктор внесены небольшие изменения:

optimize2["IntValue"] = 0;
optimize2["Целое"] = 0;
optimize2["StringValue"] = 1;
optimize2["Строка"] = 1;
optimize2["DateValue"] = 2;
optimize2["Дата"] = 2;

А индексер теперь выглядит следующим образом:

public object this[string pName]
{
  get 
  {
    object idx = (int)optimize2[pName];
    if ( idx == null )
      return null;
    switch ((int)idx)
    {
      case 0:
        return IntValue;
      case 1:
        return StringValue;
      case 2:
        return DateValue;
    }
    return null;
  }
}

Объяснять особо нечего – строим хэш-таблицу, по индексам из нее делаем switch. Владислав Чистяков и Андрей Корявченко, рецензировавшие данную статью, предполагали, что switch по строкам медленнее, чем по целочисленым типам, я полагаю так же, но пока ещё неизвестно, покроет ли ускорение switch'а замедление от использования хэш-таблицы.

Оптимизация №3 «по Владу»

Как я уже писал, этот способ получения данных предложил Владислав Чистяков, я его лишь слегка изменил (адаптировал под различные типы данных).

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

Итак, объявления делегатов:

delegate int GetInt();
delegate string GetString();
delegate DateTime GetDateTime();

Индексера у этого способа нет.

Текст инициализации перед циклом:

GetInt getInt = (GetInt)Delegate.CreateDelegate(typeof(GetInt), t,
  "get_IntValue");
GetString getString = (GetString)Delegate.CreateDelegate(typeof(GetString), 
  t, "get_StringValue");
GetDateTime getDate=(GetDateTime)Delegate.
  CreateDelegate(typeof(GetDateTime),t, "get_DateValue");

Текст цикла:

// Алгоритм не может поддерживать работу с DispName, так что...
int ti = getInt();
string si = getString();
DateTime di = getDate();
// Повторим тест два раза
int ti2 = getInt();
string si2 = getString();
DateTime di2 = getDate();

Ну, здесь опять все понятно…

Оптимизация №4 «по Владу с изменениями от автора»

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

Добавления в конструктор:

foreach (PropertyInfo pi in pis)
{
  object []attrs = pi.GetCustomAttributes(typeof(DispNameAttribute), true);
  if (attrs.Length == 0)
    continue;
  optimize1.Add(pi.Name, pi);
  Type delegateType = typeof(GetInt);
  switch (pi.PropertyType.Name)
  {
    case "string":
    case "String":
    {
      delegateType = typeof(GetString);
      break;
    }
    case "DateTime":
    {
      delegateType = typeof(GetDateTime);
      break;
    }
  }
  Delegate del=Delegate.CreateDelegate(delegateType, this, "get_" + pi.Name);
  optimize3.Add(pi.Name, del);
  foreach (DispNameAttribute attr in attrs)
  {
    if (optimize1[attr.DisplayName] != pi) 
    {
      optimize1.Add(attr.DisplayName, pi);
      optimize3.Add(attr.DisplayName, del);
    }
  }
}

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

ПРИМЕЧАНИЕ

В этом случае используются только три типа данных, в реальной жизни их может быть больше, в следующей статье, в которой будет объясняться, как получать код для switch-метода, я попытаюсь реализовать и данный метод для готового класса.

Индексер, соответственно, выглядит так:

public object this[string pName]
{
  get 
  {
    Delegate del = (Delegate)optimize3[pName];
    if (del == null)
      return null;
    else
      return del.DynamicInvoke(new object[0]);
  }
}

Здесь все просто - получаем делегат и запрашиваем значение.

Reflection Direct («Влад, Андрей и другие»)

Данный тест использует Reflection «напрямую».

ПРИМЕЧАНИЕ

Этот тест родился потому, что мне резонно заметили, что зачастую не нужно получать значения свойств по отображаемому имени. Тогда данный способ – самый «деревянный».

Используется только цикл:

// Алгоритм не может поддерживать работу с DisplayName, так что...
int ti = (int)t.ObjType.GetProperty("IntValue").GetValue(t, new object[0]);
string si = (string)t.ObjType.GetProperty("StringValue").GetValue(t,
  new object[0]);
DateTime di = (DateTime)t.ObjType.GetProperty("DateValue").GetValue(t,
  new object[0]);
// Повторим тест два раза
int ti2 = (int)t.ObjType.GetProperty("IntValue").GetValue(t, new object[0]);
string si2 = (string)t.ObjType.GetProperty("StringValue").GetValue(t,
  new object[0]);
DateTime di2 = (DateTime)t.ObjType.GetProperty("DateValue").GetValue(t,
  new object[0]);

Опять все просто – берем описание свойства по имени и получаем значение.

Reflection Indirect «Влад, Андрей и автор»

То же самое, что и предыдущий способ, но код оформлен в индексер. Сделано исключительно из желания померить накладные расходы на вызов индексера, ну и чтобы последнее слово оставить за собой. :-)

Код индексера:

public object this[string pName]
{
  get 
  {
    PropertyInfo pi = ObjType.GetProperty(pName);
    return pi.GetValue(this, new object[0]);
  }
}

И код цикла:

// Алгоритм не может поддерживать работу с DisplayName'ами, так что...
int ti = (int)t[0, 0, 0, 0, 0, "IntValue"];
string si = (string)t[0, 0, 0, 0, 0, "StringValue"];
DateTime di = (DateTime)t[0, 0, 0, 0, 0, "DateValue"];
// Повторим тест два раза
int ti2 = (int)t[0, 0, 0, 0, 0, "IntValue"];
string si2 = (string)t[0, 0, 0, 0, 0, "StringValue"];
DateTime di2 = (DateTime)t[0, 0, 0, 0, 0, "DateValue"];

Ну, вот и все.

Результаты тестов

Таблица результатов

Тип тестаИтерации / время, секОтношение
к
Способу
Direct.
1000100001000001000000
Direct0.00070.00820.07420.75491.0000
Switch0.00580.06260.65086.77928.296806
Reflection0.36223.496335.6372410.4577485.2282
Оптимизация 10.10791.295011.2538110.9073150.6808
Оптимизация 20.00430.05080.42894.27995.851424
Оптимизация Влад0.00090.01190.09240.93451.283972
Оптимизация Влад + Hacker_Delphi0.01130.12351.137911.286715.15516
Reflection Влад + Андрей0.10761.246114.6905105.6524158.9038
Reflection (Влад + Андрей + Hacker_Delphi)0.10891.169512.7182108.9690151.473

Графики


График с линейной шкалой времени. Показывает отношение времени, необходимого для выполнения той или иной операции, но рост времени отображается не совсем верно по причине нелинейности роста количества итераций в нашем тесте.


А вот этот график совсем не показывает отношения времени выполнения различных операций, зато показывает темпы роста времени операции в зависимости от количества итераций.

Итоги

Итак, подведем итоги.

Мы проверили восемь способов получения данных из объектов в .Net Framework и сравнили их быстродействие с быстродействием прямых обращений к свойствам.

Результаты вполне утешают:

  1. Мы имеем очень быстрый способ обращения к данным объекта «через Reflection» – используя оригинальные имена свойств (способ от Владислава Чистякова). Как видно из следующего за ним способа, вполне возможно его использовать, не «закладывая» класс объекта на этапе компиляции.
  2. Мы имеем два достаточно быстрых способа обращения к данным по «ненастоящим именам» (Switch и оптимизированый Switch). Неоптимизированый способ хорош тем, что не требует наличия дополнительной памяти на каждый экземпляр объекта (в принципе, можно побороть и это, сделав фабрику хэш-таблиц, к которой нужно обращаться в момент создания объекта. Тогда и второй способ будет иметь минимальные затраты «лишней» памяти.
  3. Мы точно знаем, какими способами для доступа к данным пользоваться не нужно, например, прямой работой с Reflection.

Самое главное, мы теперь знаем, что есть способы использования технологии Reflection, достаточно быстрые для применения в интерактивных приложениях.


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь
    Сообщений 14    Оценка 426        Оценить