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

Скорость Reflection .Net

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

Автор: Михаил Полюдов
Источник: RSDN Magazine #5-2003
Опубликовано: 11.04.2004
Исправлено: 10.12.2016
Версия текста: 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
{
  privatestring _displayName;
  publicstring DisplayName
  {
    get { return _displayName; }
  }
  public DispNameAttribute(string DisplayName)
  {
    _displayName = DisplayName;
  }
}

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

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

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

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

Имя свойства Тип свойства Отображаемое имя Значение
IntValue int (System.Int32) Целое 1
StringValue string (System.String) Строка «Строка!»
DateValue DateTime (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++)
{
  // Здесь – обращение к данным
}
floatFinishTime = 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;
      }
    }
    returnnull;
  }
}

Данный код просто делает 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, newobject[0]);
      elsefor (int i = 0; i < attrs.Length; i++) 
        {  
          if (((DispNameAttribute)attrs[i]).DisplayName == pName)
          {
            return pi.GetValue(this, newobject[0]);
          }
        }
    }
    returnnull;
  }
}

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

ПРИМЕЧАНИЕ

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

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

Оптимизация №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)
      returnnull;
    elsereturn pi.GetValue(this, newobject[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 )
      returnnull;
    switch ((int)idx)
    {
      case 0:
        return IntValue;
      case 1:
        return StringValue;
      case 2:
        return DateValue;
    }
    returnnull;
  }
}

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

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

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

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

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

        delegate
        int GetInt();
delegatestring 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)
      returnnull;
    elsereturn del.DynamicInvoke(newobject[0]);
  }
}

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

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

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

ПРИМЕЧАНИЕ

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

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

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

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

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

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

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

        public
        object
        this[string pName]
{
  get 
  {
    PropertyInfo pi = ObjType.GetProperty(pName);
    return pi.GetValue(this, newobject[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.
1000 10000 100000 1000000
Direct 0.0007 0.0082 0.0742 0.7549 1.0000
Switch 0.0058 0.0626 0.6508 6.7792 8.296806
Reflection 0.3622 3.4963 35.6372 410.4577 485.2282
Оптимизация 1 0.1079 1.2950 11.2538 110.9073 150.6808
Оптимизация 2 0.0043 0.0508 0.4289 4.2799 5.851424
Оптимизация Влад 0.0009 0.0119 0.0924 0.9345 1.283972
Оптимизация Влад + Hacker_Delphi 0.0113 0.1235 1.1379 11.2867 15.15516
Reflection Влад + Андрей 0.1076 1.2461 14.6905 105.6524 158.9038
Reflection (Влад + Андрей + Hacker_Delphi) 0.1089 1.1695 12.7182 108.9690 151.473

Графики


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


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

Итоги

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

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

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

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

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


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