![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Плавно, Ускоренно,
Быстро, Быстрее,
Быстро, как только можете,
Быстрее, Еще быстрее… Записки на нотах.
Перед программистом зачастую стоит задача получения информации из объектов «по имени», то есть получения информации через имя свойства, которое неизвестно заранее, либо задано каким-либо атрибутом. Примером может служить 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, по количеству тестов.
Для данного способа есть только код цикла:
// Алгоритм не может поддерживать работу с 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; |
С этим способом, в общем-то, все ясно. Данный тест выполняется исключительно для того, чтобы получить «эталонное» значение для сравнения с остальными способами получения данных.
Для этого теста используется индексер:
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["Дата"]; |
Я думаю, без излишних комментариев понятно, что делает этот код.
ПРИМЕЧАНИЕ Далее по тексту будет использоваться ссылка на этот код как на «цикл для свойств с отображаемыми именами». |
Тот самый «деревянный» способ:
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; } } |
Данный алгоритм просто перебирает все описания свойств и проверяет, не найдено ли нужное свойство.
ПРИМЕЧАНИЕ Порядок проверки длины массива атрибутов и имени свойства – не ошибка… просто передо мной стояла задача получать значения именно «обатрибученых» свойств. |
Предваряя график и таблицу результатов тестирования, скажу: НИКОГДА НЕ ИСПОЛЬЗУЙТЕ ЭТОТ СПОСОБ ДОСТУПА К СВОЙСТВАМ ПО ИМЕНАМ, ну кроме, разве что, случаев, когда вам нужно написать код быстро, а быстродействие вас не волнует. |
Объявление дополнительных полей:
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 и получает значение свойства.
Цикл – стандартный для отображаемых имен.
Этот способ – некая комбинация предыдущего и 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'а замедление от использования хэш-таблицы.
Как я уже писал, этот способ получения данных предложил Владислав Чистяков, я его лишь слегка изменил (адаптировал под различные типы данных).
Суть этого метода заключается в том, что можно создать делегат, обработчиком которого будет метод чтения соответствующего свойства. Способ весьма интересный, и (забегая вперед) самый близкий по времени выполнения к прямому доступу.
Итак, объявления делегатов:
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(); |
Ну, здесь опять все понятно…
Попробуем устранить недостаток предыдущего алгоритма, заключающийся в отсутствии индексера.
Добавления в конструктор:
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 «напрямую».
ПРИМЕЧАНИЕ Этот тест родился потому, что мне резонно заметили, что зачастую не нужно получать значения свойств по отображаемому имени. Тогда данный способ – самый «деревянный». |
Используется только цикл:
// Алгоритм не может поддерживать работу с 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]); |
Опять все просто – берем описание свойства по имени и получаем значение.
То же самое, что и предыдущий способ, но код оформлен в индексер. Сделано исключительно из желания померить накладные расходы на вызов индексера, ну и чтобы последнее слово оставить за собой. :-)
Код индексера:
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 и сравнили их быстродействие с быстродействием прямых обращений к свойствам.
Результаты вполне утешают:
Самое главное, мы теперь знаем, что есть способы использования технологии Reflection, достаточно быстрые для применения в интерактивных приложениях.
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |