Високопродуктивний код на платформі .NET

Здрастуйте, дорогі читачі!

Не так давно ми зайнялися обробкою книги "Writing High-Performance .NET code", яка досі не перекладена на російську мову, хоча їй і скоро рік.



Нас, звичайно, не здивувало, що таку книгу вже розтягують на цитати, однак з'ясувалося, що шановний автор Бен Уотсон, навіть виклав на сайті «проект коду (codeproject)» цілу статті, написану за мотивами однієї з глав. На жаль, обсяг цього матеріалу дуже великий для хабропубликации, однак ми вирішили все-таки перевести першу частину статті, щоб ви могли оцінити матеріал книги. Запрошуємо до прочитання і до участі в опитуванні. Крім того, якщо все-таки доцільно перевести і другу частину — пишіть в коментарях, постараємося врахувати ваші побажання.


Контекст

Ця стаття написана на основі глави 5 із моєї книги Writing High-Performance .NET Code.

Введення

У статті розглянуто загальні принципи написання коду та дизайну типів. На платформі .NET є можливості для реалізації різноманітних сценаріїв, і якщо деякі з них в кращому разі ніяк не позначаються на продуктивності, є й такі, які серйозно її псують. Певні сценарії ні хороші, ні погані, а просто такі, які вони є. Доводиться підбирати найбільш адекватне рішення для кожної ситуації.

Якщо б я спробував сформулювати загальний принцип, що лежить в основі цієї статті, то він був би такий:

Глибока оптимізація продуктивності найчастіше порушує програмні абстракції.

Це означає, що, намагаючись досягти екстремально високої продуктивності, ми повинні розуміти деталі реалізації на всіх рівнях і, можливо, намагатися зіграти на цих тонкощах. Багато подібні деталі описані в цій статті.

Порівняння класів і структур

Екземпляри класу завжди виділяються в купі, а доступ до цих екземплярів здійснюється шляхом розіменування вказівника. Передавати їх дешево, адже йдеться лише про копії покажчика (4 або 8 байт). Однак у об'єкта також є деякі фіксовані витрати: 8 байт для 32-бітних процесів і 16 байт для 64-бітних процесів. У ці витрати входить покажчик на таблицю методів плюс поле блоку синхронізації. Якщо створити об'єкт без полів і переглянути його у налагоджувач, то виявиться, що насправді його розмір складає не 8, а 12 байт. У випадку з 64-бітними процесами об'єкт буде мати розмір 24 байт. Справа в тому, що мінімальний розмір залежить від вирівнювання блоків пам'яті. На щастя, ці «зайві» 4 байт будуть використовуватися полем.

Структура struct не тягне витрат, а розмір використовуваної нею пам'яті — це сума розмірів всіх її полів. Якщо struct оголошується як локальна змінна в методі, то struct виділяється в стеку. Якщо struct оголошується як частина класу, то пам'ять struct буде входити до складу фрагмента пам'яті, займаної цим класом (і, отже, перебувати в купі). Якщо передати структуру struct методом, то вона буде послідовно скопійована байт за байтом. Оскільки вона не перебуває в купі, виділення структури ніколи не ініціює збирання сміття.

Отже, тут доводиться йти на компроміс. Ви можете зустріти різні рекомендації про максимальному розмірі структури, але я б не прив'язувався до конкретного числа. Як правило, розмір struct краще тримати дуже невеликим, особливо якщо ця структура передається туди-сюди, проте структури можна передавати і за посиланням, тому її розмір може не представляти суттєвої проблеми. Єдиний спосіб впевнено визначити, корисний чи буде такий прийом — уважно придивитися до паттерну використання і самостійно виконати профілювання.

У деяких ситуаціях ефективність може разюче відрізнятися. Нехай величина витрат на окремо взятий об'єкт і здається мізерною, але спробуйте розглянути масив об'єктів, а потім порівняєте його з масивом структур. Припустимо, у структурі даних міститься 16 байт даних, довжина масиву дорівнює 1 000 000, і ми працюємо у 32-розрядній системі.

Для масиву об'єктів загальний витрата простору становить:

12 байт витрат масиву +
(розмір вказівника 4 байт × 1,000,000) +
(( витрати 8 байт + 16 байт даних) × 1,000,000)
= 28 MB

Для масиву структур отримуємо принципово інший результат:

12 байт витрат масиву +
(16 байт даних × 1,000,000)
= 16 MB

У випадку з 64-бітним процесом масив об'єктів займає більше 40 MB, тоді як масив struct вимагає всього 16 MB.

Як бачите, у масиві struct аналогічний обсяг даних займає менше пам'яті, ніж у масиві об'єктів. Разом з витратами, пов'язаними з використанням об'єктів, ви також отримуєте більш інтенсивну збірку сміття, що пояснюється більш високим навантаженням на пам'ять.

Крім використання простору є ще і питання ефективності процесора. Процесор має кілька рівнів кешування. Ті, що розташовані ближче всього до процесора, дуже невеликі, але працюють виключно швидко і оптимізовані для послідовного доступу.

Масив struct містить безліч значень, послідовно розташованих в пам'яті. Звернутися до елементу в масиві struct дуже просто. Як тільки коректна запис знайдено, у нас вже є вірне значення. Таким чином, може виникати величезна різниця в швидкості доступу при переборі великого масиву. Якщо значення вже знаходиться в кеші процесора, то до нього можна звернутися на порядок швидше, ніж якщо воно розташоване в оперативній пам'яті.

Для доступу до елементу об'єктної масиву потрібно звернутися до пам'яті масиву, а потім разыменовать вказівник миші на елемент, який може розташовуватися в будь-якому місці купи. Перебір об'єктних масивів пов'язаний з разыменованием додаткового покажчика, «стрибками» по купі і порівняно частим спустошенням кешу процесора, що потенційно призводить до розтрати більше потрібних даних.

Відсутність подібних витрат як на рівні процесора, так і в пам'яті — у багатьох випадках основна причина для переваги структур. При розумному використанні такий прийом може дати значний виграш у продуктивності, так як поліпшується локальність пам'яті.

Оскільки структури завжди копіюються за значенням, можна за необережності потрапити і в більш цікаве положення. Наприклад, наступний код написаний з помилками і не відбудеться створення:

struct Point
{
public int x;
public int y;
}

public static void Main()
{
List<Point> points = new List<Point>();
points.Add(new Point() { x = 1, y = 2 });
points[0].x = 3;
}


Проблема виникає в останньому рядку, яка намагається змінити існуючий Point у списку. Це неможливо, оскільки викликpoints[0] повертає копію оригінального значення, яка ніде додатково не зберігається. Правильний спосіб зміни Point:

Point p = points[0];
p.x = 3;
points[0] = p;


Однак може бути доцільно реалізувати ще більш сувору політику: структури незмінними. Після створення структура така структура вже не зможе отримати нове значення. У такому разі вищеописана ситуація стає в принципі неможлива, вся робота зі структурами спрощується.

Вище згадувалося, що структури повинні бути компактними, щоб не довелося витрачати багато часу на їх копіювання, але іноді доводиться використовувати і великі структури. Розглянемо об'єкт, в якому відстежується маса деталей про якомусь комерційному процесі, наприклад, ставиться безліч тимчасових міток.

class Order
{
public DateTime ReceivedTime {get;set;}
public DateTime AcknowledgeTime {get;set;}
public DateTime ProcessBeginTime {get;set;}
public DateTime WarehouseReceiveTime {get;set;}
public DateTime WarehouseRunnerReceiveTime {get;set;}
public DateTime WarehouseRunnerCompletionTime {get;set;}
public DateTime PackingBeginTime {get;set;}
public DateTime PackingEndTime {get;set;}
public DateTime LabelPrintTime {get;set;}
public DateTime CarrierNotifyTime {get;set;}
public DateTime ProcessEndTime {get;set;}
public DateTime EmailSentToCustomerTime {get;set;}
public DateTime CarrerPickupTime {get;set;}

// безліч інших даних ...
}


Щоб спростити код, було б непогано виділити кожну з таких міток у власну підструктуру, яка як і раніше буде доступна через код класу Order наступним чином:

Order order = new Order();
Order.Times.ReceivedTime = DateTime.UtcNow;


Всі ці підструктури можна винести в окремий клас:

class OrderTimes
{
public DateTime ReceivedTime {get;set;}
public DateTime AcknowledgeTime {get;set;}
public DateTime ProcessBeginTime {get;set;}
public DateTime WarehouseReceiveTime {get;set;}
public DateTime WarehouseRunnerReceiveTime {get;set;}
public DateTime WarehouseRunnerCompletionTime {get;set;}
public DateTime PackingBeginTime {get;set;}
public DateTime PackingEndTime {get;set;}
public DateTime LabelPrintTime {get;set;}
public DateTime CarrierNotifyTime {get;set;}
public DateTime ProcessEndTime {get;set;}
public DateTime EmailSentToCustomerTime {get;set;}
public DateTime CarrerPickupTime {get;set;}
}

class Order
{
public OrderTimes Times;
}


Однак при цьому додатково виникають 12 або 24 байт витрат на кожен об'єкт Order.

Якщо вам потрібно передавати різним методам об'єкт OrderTimes цілком, то, можливо, такі витрати виправдані, але чому б просто не передати посилання на цілий об'єкт Order? Якщо у вас одночасно обробляються тисячі об'єктів Order, це призведе до значної активізації сміття. Крім того, в пам'яті підуть додаткові операції розіменування.

Спробуйте краще зробити OrderTimes структурою. Звернення до окремих властивостей структури OrderTimes через властивість об'єкта Order (наприклад, order.Times.ReceivedTime) не призводить до копіювання структури (в .NET цей вірогідний сценарій спеціально оптимізовано). Таким чином, структура OrderTimes в сутності входить до складу ділянки пам'яті, відведеної під клас Order, немов тут і немає ніякої підструктури. Сам код при цьому також стає акуратніше.

Така техніка не порушує принципу незмінних структур, але весь фокус тут полягає в наступному: ми звертаємося з полями структури OrderTimes точно як якщо б вони були полями об'єкта Order. Якщо вам не потрібно передавати структуру OrderTimes як цілісну сутність, то запропонований механізм є суто організаційним.

Перевизначення методів Equals і GetHashCode для структур

При роботі зі структурами виключно важливо перевизначати методи Дорівнює GetHashCode. Якщо цього не зробити, то ви отримаєте їх версії, що задаються за умовчанням, які аж ніяк не сприяють високій продуктивності. Щоб оцінити, наскільки це недобре, відкрийте переглядач проміжного мови і погляньте на код методу ValueType.Equals. Він пов'язаний з рефлексією по всіх полях структури. Однак це оптимізація для двійково-сумісних типів. Двійково-сумісним (blittable) називається такий тип, який має однакове уявлення в пам'яті як в керованому, так і в некерованому коді. До їх числа відносяться тільки примітивні числові типи (наприклад,Int32, UInt64, а не Decimal, не є примітивним) іIntPtr/UIntPtr. Якщо структура складається тільки з двійково-сумісних типів, то реалізація Equals може фактично виконувати побайтное порівняння пам'яті в рамках всієї структури. Просто уникайте такої невизначеності і реалізуйте власний методДорівнює.

Якщо просто викласти Equals(object other), то у вас все одно вийде невиправдано низька продуктивність, оскільки цей метод пов'язаний з приведенням і упаковкою типів значень. Замість цього реалізуйте Equals(T other), де T – тип вашої структури. Для цього призначений інтерфейс IEquatable, і всі структури повинні реалізовувати його. Компілятор при роботі завжди віддає перевагу більш строго типизированной версії, якщо це можливо. Розглянемо приклад:

struct Vector : IEquatable<Vector>
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }

public int Magnitude { get; set; }

public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj.GetType() != this.GetType())
{
return false;
}
return this.Equals((Vector)obj);
}

public bool Equals(Vector other)
{
return this.X == other.X
&& this.Y == other.Y
&& this.Z == other.Z
&& this.Magnitude == other.Magnitude;
}

public override int GetHashCode()
{
return X ^ Y ^ Z ^ Magnitude;
}
}


Якщо тип реалізує інтерфейс IEquatable, то узагальнені колекції .NET його виявлять і стануть використовувати для більш ефективного пошуку і сортування.

Можливо, ви вирішите застосовувати оператори == і != з типами значень і змусите їх викликати метод existingEquals(T).

Навіть якщо ви ніколи не порівнюєте структури або не кладете їх в колекції, все-таки рекомендую реалізувати ці методи. Ніколи не вгадаєш, як вони будуть використовуватися в майбутньому, а на написання методів піде всього кілька хвилин і лічені байти проміжного мови, які навіть ніколи не вимагатимуть динамічної компіляції.

Перевизначати методи Дорівнює GetHashCode у класів не настільки важливо, оскільки в даному випадку вони лише обчислюють рівність, виходячи з посилання на об'єкт. Якщо ви вважаєте, що у вашому коді, буде цілком достатньо стандартної реалізації цих методів — не міняйте її.

— Віртуальні методи і запечатані класи

Не робіть методи віртуальними за замовчуванням «на всяк випадок». Однак, якщо віртуальні методи потрібні для погодженого дизайну ваших програм, то, мабуть, не перестарайтеся з їх видаленням.

Якщо зробити методи віртуальними, це перешкоджає визначеним оптимізацій з боку динамічного компілятора, зокрема, заважає вбудовувати методи. Методи можуть вбудовуватися лише в тому випадку, якщо компілятору на 100% відомо, який метод буде викликатися. Позначаючи метод як віртуальний, ми втрачаємо таку визначеність, хоча, існують і інші фактори, які можуть змусити відмовитися від такої оптимізації.

До віртуальних методів концептуально близькі запечатані класи, наприклад:

public sealed class MyClass {}


Клас, позначений як запечатаний, оголошує, що ніякі інші класи не можуть від нього успадковувати.

Теоретично, динамічний компілятор може використовувати цю інформацію і більш активно займатися вбудовуванням, але в даний час цього не відбувається. Як би те ні було, слід за замовчуванням оголошувати класи як запечатані та не робити методи за замовчуванням віртуальними, якщо в цьому немає необхідності. У такому випадку ваш код буде пристосований до будь-яких актуальних оптимізацій динамічного компілятора, а також до тих, які можливі в майбутньому.

Якщо ви пишете бібліотеку класів, яку передбачається використовувати в різноманітних ситуаціях, особливо поза вашої організації, слід діяти обережніше. У такому випадку наявність віртуального API може бути важливіше голою продуктивності – щоб ваша бібліотека була зручна для перевикористання і настроювання. Але якщо код пишеться для внутрішньокорпоративних потреб і часто змінюється, намагайтеся забезпечити більш високу продуктивність.

Диспетчеризація інтерфейсів

Коли ви в перший раз викликаєте метод через інтерфейс .NET необхідно визначити, до якого типу та методу робити виклик. Спочатку робиться виклик до заглушки, яка знаходить метод, що викликається при роботі з об'єктом, що реалізує цей інтерфейс. Після того, як це відбудеться кілька разів, CLR «засвоїть», що весь час викликається один і той же конкретний тип, і цей непрямий виклик через заглушку редукується до заглушки, що складається всього з декількох інструкцій складання, викликає потрібний метод. Така група інструкцій називається «мономорфной заглушкою», оскільки вона знає, як викликати метод лише для одного-єдиного типу. Це ідеально в ситуаціях, коли місце виклику завжди викликає інтерфейсні методи на одному і тому ж типі.

Мономорфная заглушка також дозволяє виявити, якщо щось піде неправильно. Якщо в якийсь момент місце виклику почне використовувати метод іншого типу, то CLR в результаті замінить заглушку інший мономорфной заглушкою для нового типу.

Якщо ситуація ще складніше, в неї залучені кілька типів, і вона менш передбачувана (наприклад, є масив інтерфейсного типу, але в цьому масиві присутні кілька конкретних типів), то заглушка перетвориться в поліморфну і буде використовувати хеш-таблицю, що дозволяє вибирати, який метод викликати. Пошук по таблиці швидкий, але все-таки не настільки, як при роботі з мономорфной заглушкою. Крім того, розмір такої хеш-таблиці суворо обмежений, і якщо у вас занадто багато типів, то, можливо, доведеться з самого початку відкотитися до узагальненого кодом пошуку типів. Ця операція може виявитися дуже витратною.

Якщо така проблема виникне, її можна вирішити одним із двох способів:

  1. 1. Уникайте викликати ці об'єкти через загальний інтерфейс
  2. 2. Виберіть загальний базовий інтерфейс і замініть його базовим класом abstract


Подібна проблема зустрічається не часто, однак вона може трапитися, якщо у вас є величезна ієрархія типів, всі вони реалізують спільний інтерфейс, і ви викликаєте методи через цей загальний інтерфейс. Можна помітити, що процесор працює на місці виклику цих методів настільки активно, що це неможливо пояснити лише тією роботою, яку роблять самі методи.

Історія При проектуванні великої системи ми заздалегідь знали, що у нас, можливо, будуть тисячі різних типів, і всі вони будуть відбуватися від загального типу. У парі місць у нас повинна була виникнути необхідність звертатися до них з базового типу. Оскільки в нашій команді була людина, розбирався в диспетчеризації інтерфейсів при вирішенні проблем такого масштабу, ми вирішили використовувати базовий клас abstract, а не кореневої інтерфейс.

Хороша стаття про диспетчеризації інтерфейсів є в блозі Ванса Моррісона.

Уникайте упаковки

Упаковка – це процес обгортання значущого типу, наприклад, примітиву або структури, об'єкт, що знаходиться в купі. У такому вигляді цей тип може передаватися методів, що вимагають посилання на об'єкт. Розпакування — це витяг вихідного значення.

На упаковку витрачається процесорний час, необхідне виділення, копіювання і приведення об'єкта. Однак ще важливіше, що при цьому в купі активізується збірка сміття. Якщо недбало ставитися до упаковки, то в програмі може виникнути безліч операцій виділення, і всі вони нададуть додаткову навантаження на збирач сміття.
Явна упаковка відбувається всякий раз, коли виконуються такі операції:

int x = 32;
object o = x;


Проміжний мова тут виглядає так:

IL_0001: ldc.i4.s 32
IL_0003: stloc.0
IL_0004: ldloc.0
IL_0005: box [mscorlib]System.Int32
IL_000a: stloc.1


Це означає, що відносно нескладно знайти більшість джерел упаковки у вашому коді: просто скористайтесь ILDASM для перетворення всього вашого IL текст і виконайте пошук.

Дуже поширена ситуація, в якій можлива нерегулярна упаковка — це використання API, приймаючого object або object[] в якості параметра. Найбільш тривіальними з них є String.Format або традиційні колекції, в яких зберігаються тільки посилання на об'єкти, роботи за якими потрібно повністю уникнути з тієї чи іншої причини.

Крім того, упаковка може відбуватися при присвоєнні структури інтерфейсу, наприклад:

interface INameable
{
string Name { get; set; }
}

struct Foo : INameable
{
public string Name { get; set; } 
}

void TestBoxing()
{ 
Foo foo = new Foo() { Name = "Bar" };
// упаковка!
INameable nameable = foo;
...
}


Якщо вирішите протестувати цей код – зверніть увагу, що якщо ви не використовуєте упаковану змінну, то компілятор в порядку оптимізації просто прибере інструкцію упаковки, оскільки вона так і не задіюється. Як тільки ви викличете метод чи використовуєте значення ще якимось чином, інструкція упаковки буде на місці.

Ще одна річ, яка відбувається при упаковці і яку необхідно враховувати – результат наступного коду:

int val = 13;
object boxedVal = val;
val = 14;


Яким буде значення boxedVal після цього?

При упаковці значення копіюється, і між оригіналом і копією не залишається ніякого зв'язку. Наприклад, значення val може змінитися на 14, але boxedVal збереже початкове значення 13.

Іноді упаковку вдається відстежити в профілі процесора, але багато пакувальні виклики просто вбудовуються, і надійного способу знайти їх не існує. При надмірній упаковці ви побачите в профілі процесора лише масове виділення пам'яті за допомогою new.

Якщо вам доводиться активно упаковувати структури, і ви приходите до висновку, що без цього не обійтися, то, ймовірно, просто слід перетворити структуру в клас, що може виявитися навіть дешевше.

Нарешті, зверніть увагу: передача значущого типу за посиланням упаковкою не є. Подивіться IL і переконайтеся, що жодної упаковки не відбувається. Адреса значущого типу відправляється методом.

Порівняння for і foreach

Використовуйте програму MeasureIt, щоб оцінити, чим відрізняється перебір колекцій в циклах for foreach. Робота зі стандартними циклами for протікає значно швидше практично в будь-яких випадках. Проте якщо ви спробуєте провести власний простий тест, то, залежно від сценарію, можете виявити, що обидва циклу мають приблизно рівну продуктивність. Дійсно, у багатьох ситуаціях .NET перетворює прості інструкції foreach в стандартні цикли for.

Розглянемо проект ForEachVsFor, в якому є наступний код:

int[] arr = new int[100];
for (int i = 0; i < arr.Length; i++)
{
arr[i] = i;
}

int sum = 0;
foreach (int in val arr)
{
sum += val;
}

sum = 0;
IEnumerable<int> arrEnum = arr;
foreach (int in val arrEnum)
{
sum += val;
}


Зібравши його, спробуйте декомпілювати цей код за допомогою інструменту для рефлексії IL. Ви побачите, що перший foreach насправді компілюється як цикл for. Проміжний мова буде такий:

// Початок циклу (head: IL_0034)
IL_0024: ldloc.s CS$6$0000
IL_0026: ldloc.s CS$7$0001
IL_0028: ldelem.i4
IL_0029: stloc.3
IL_002a: ldloc.2
IL_002b: ldloc.3
IL_002c: add
IL_002d: stloc.2
IL_002e: ldloc.s CS$7$0001
IL_0030: ldc.i4.1
IL_0031: add
IL_0032: stloc.s CS$7$0001
IL_0034: ldloc.s CS$7$0001
IL_0036: ldloc.s CS$6$0000
IL_0038: ldlen
IL_0039: conv.i4
IL_003a: blt.s IL_0024
// Кінець циклу


Тут багато операцій збереження, завантаження, додавання і одна гілка – все досить просто.

Проте варто нам привести масив до IEnumerable і виконати ту ж операцію, як робота виявляється значно більш витратною:

IL_0043: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0048: stloc.s CS$5$0002
.try
{
IL_004a: br.s IL_005a
// початок циклу (head: IL_005a)
IL_004c: ldloc.s CS$5$0002
IL_004e: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0053: stloc.s val
IL_0055: ldloc.2
IL_0056: ldloc.s val
IL_0058: add
IL_0059: stloc.2

IL_005a: ldloc.s CS$5$0002
IL_005c: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_0061: brtrue.s IL_004c
// кінець циклу

IL_0063: leave.s IL_0071
} // кінець блоку .try
finally
{
IL_0065: ldloc.s CS$5$0002
IL_0067: brfalse.s IL_0070

IL_0069: ldloc.s CS$5$0002
IL_006b: callvirt instance void [mscorlib]System.IDisposable::Dispose()

IL_0070: endfinally
} // кінець обробника


У нас 4 виклику віртуальних методів, блок try-finally і (тут не показано) виділення пам'яті для локальної змінної нумератор, в якій відстежується стан перерахування. Така операція набагато затратніше, ніж звичайний цикл for: використовується більше процесорного часу і більше пам'яті!

Не забувайте, що базова структура даних тут — як і раніше масив, а отже, цикл for використовувати можна, але ми йдемо на обфускацію, приводячи тип інтерфейсу IEnumerable. Тут важливо враховувати факт, вже згадуваний на початку статті: глибока оптимізація продуктивності часто йде врозріз з абстракціями коду. Так, foreach — це абстракція циклу, IEnumerable – абстракція колекції. Разом вони дають таку поведінку, що виключають просту оптимізацію із застосуванням циклуfor, перебирає масив.

Приведення

В принципі, слід по можливості уникати приведення. Приведення часто свідчить про неякісне дизайні класів, але іноді воно дійсно потрібно. Так, до приведення доводиться вдаватися при перетворенні чисел зі знаком в беззнакові, наприклад, коли ми працюємо з різними сторонніми API. Приведення об'єктів має відбуватися набагато рідше.

Приведення об'єктів ніколи не обходиться без витрат, але вартість такої операції може разюче відрізнятися в залежності від відносин між об'єктами. Привести предка до потрібного нащадку значно затратніше, ніж виконати зворотну операцію, і вартість такої операції тим вище, чим більше ієрархія. Приведення до інтерфейсу обходиться дорожче, ніж приведення до конкретного типу.

Абсолютно неприпустимо невірне приведення. Якщо воно відбудеться, ви отримаєте виключення InvalidCastException, вартість якого на порядки перевищить ціну операції приведення.

См. проект CastingPerf у вихідному коді до цієї книги, де зазначено кількість привидів тих чи інших типів.

При тестовому прогоні на моєму комп'ютері вийшов такий результат:

JIT (ignore): 1.00 x
No cast: 1.00 x
Up cast (1 gen): 1.00 x
Up cast (2 gens): 1.00 x
Up cast (3 gens): 1.00 x
Down cast (1 gen): 1.25 x
Down cast (2 gens): 1.37 x
Down cast (3 gens): 1.37 x
Interface: 2.73 x
Invalid Cast: 14934.51 x
as (success): 1.01 x
as (failure): 2.60 x
is (success): 2.00 x
is (failure): 1.98 x


Оператор 'is' — це приведення, тестуюча результат і повертає булеве значення.

Оператор 'as' нагадує стандартне приведення, але повертає null, приведення якщо не спрацьовує. Як видно з вищенаведених результатів, ця операція набагато швидше, ніж видача виключення.
Ніколи не застосовуйте такий патерн, де робиться два приведення:

if (a is Foo)
{
Foo f = (Foo)a;
}


Краще використовуйте для приведення 'as' і кэшируйте результат, а потім перевіряйте обчислене значення:

Foo f = a as Foo;
if (f != null)
{
...
}


Якщо доводиться виконати перевірку для безлічі типів, то спочатку вказуйте найбільш поширений тип.

Примітка: Мені доводиться регулярно стикатися з одним неприємним перетворенням, коли використовується MemoryStream.Length типу long. Більшість API, стикаються з цим, використовують посилання на базовий буфер (видобуту з методу MemoryStream.GetBuffer), зміщення і довжину, яка часто відноситься до типу int. Таким чином, виникає необхідність спадного перетворення від long. Подібні перетворення можуть бути регулярними і неминучими.
Враження про книги

/>
/>


<input type=«radio» id=«vv67595»
class=«radio js-field-data»
name=«variant[]»
value=«67595» />
Так, куплю цю книгу російською мовою
<input type=«radio» id=«vv67597»
class=«radio js-field-data»
name=«variant[]»
value=«67597» />
Не куплю, достатньо почитати блоги
<input type=«radio» id=«vv67599»
class=«radio js-field-data»
name=«variant[]»
value=«67599» />
Читав (читаю) в оригіналі

Проголосувало 48 осіб. Утрималося 22 людини.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.