Покращуємо продуктивність: boxing в. NET, якого можна уникнути

Ми у своєму проекті займаємося розробкою сервера на C #. Цей сервер повинен витримувати дуже високі навантаження, з цієї причини ми намагаємося написати код якомога оптимальніше. C # рідко асоціюють з високою продуктивністю, але якщо з розумом підходити до розробки, то можна досягти дуже навіть непоганого рівня.
 
Одним з недешевих процесів з точки зору продуктивності є boxing і unboxing. Напоминалку про те, що це таке, можна знайти тут . Нещодавно я вирішив подивитися весь IL код наших проектів і пошукати інструкції box і unbox. Знайшлося чимало ділянок, boxing'а в яких можна уникнути легким рухом руки. Всі випадки, що призводять до непотрібного boxing'у, очевидні, і допускаються за неуважності в моменти концентрації на функціональності, а не на оптимізації. Я вирішив виписати найбільш часто зустрічаються випадки, щоб не забувати про них, а потім автоматизувати їх виправлення. У даній статті і перераховані ці випадки.
 
Відразу зроблю ремарку: найчастіше проблеми продуктивності лежать на більш високому рівні, і перш ніж правити весь зайвий boxing, потрібно привести код до такого стану, коли від цього буде толк. Серйозно замислюватися про такі речі як boxing має сенс, якщо ви дійсно хочете вичавити максимум з C #.
 
Хай вибачить мене російська мова, але далі в статті я буду використовувати несподіване для нього слово «боксинг», щоб око не чіплявся зайвий раз в спробі знайти рядок коду.
 
Приступимо.
 
 
1. Передача value type змінних в методи String.Format, String.Concat і т.п.
Перше місце за кількістю боксингу тримають рядкові операції. Благо, в нашому коді це зустрічалося в основному у форматуванні повідомлення для винятків. Основне правило для уникнення боксингу — це викликати ToString () у value type змінної перед використанням в методах String.Format або при додаванні рядків.
 
Те ж саме, але в коді. Замість:
 
 
var id = Guid.NewGuid();
var str1 = String.Format("Id {0}", id);
var str2 = "Id " + id;

 
 
IL_0000: call valuetype [mscorlib]System.Guid [mscorlib]System.Guid::NewGuid()
IL_0005: stloc.0
IL_0006: ldstr "Id {0}"
IL_000b: ldloc.0
IL_000c: box [mscorlib]System.Guid
IL_0011: call string [mscorlib]System.String::Format(string, object)
IL_0016: pop
IL_0017: ldstr "Id "
IL_001c: ldloc.0
IL_001d: box [mscorlib]System.Guid
IL_0022: call string [mscorlib]System.String::Concat(object, object)

 
Потрібно писати:
 
 
var id = Guid.NewGuid();
var str1 = String.Format("Id {0}", id.ToString());
var str2 = "Id " + id.ToString();

 
 
IL_0000: call valuetype [mscorlib]System.Guid [mscorlib]System.Guid::NewGuid()
IL_0005: stloc.0
IL_0006: ldstr "Id {0}"
IL_000b: ldloca.s id
IL_000d: constrained. [mscorlib]System.Guid
IL_0013: callvirt instance string [mscorlib]System.Object::ToString()
IL_0018: call string [mscorlib]System.String::Format(string, object)
IL_001d: pop
IL_001e: ldstr "Id "
IL_0023: ldloca.s id
IL_0025: constrained. [mscorlib]System.Guid
IL_002b: callvirt instance string [mscorlib]System.Object::ToString()
IL_0030: call string [mscorlib]System.String::Concat(string, string)

 
Як ми бачимо, з'являється інструкція constrained замість box. Тут написано, що наступний виклик callvirt буде прямо у змінної, за умови, що thisType це value type, і є реалізація методу. Якщо ж реалізації методу немає, то все одно відбудеться боксинг.
 
Неприємний момент полягає в тому, що майже у всіх стоїть Resharper, який підказує, що виклик ToString () зайвий.
 
І ще щодо рядків, а точніше їх складання. Іноді зустрічав код кшталт:
 
 
var str2 = str1 + '\t';

 
Є помилкове відчуття, що
char
без проблем складеться з рядком, але
char
— це value type, тому тут теж буде боксинг. У цьому випадку все-таки краще писати так:
 
 
var str2 = str1 + "\t";

 
 
2. Виклик методів на generic змінних
Друге місце за кількістю боксингу тримають generic методи. Справа в тому, що будь-який виклик методу на generic змінної викликає боксинг, навіть за умови, що виставлений constraint
class
.
 
Приклад:
 
 
public static Boolean Equals<T>(T x, T y)
	where T : class 
{
	return x == y;
}

 
Перетворюється в:
 
 
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: ldarg.1
IL_0007: box !!T
IL_000c: ceq

 
Насправді тут не все так погано, так як даний IL код буде прооптімізірован JIT'ом, але випадок цікавий.
 
Позитивним моментом є також те, що для виклику методів на generic змінних використовується вже знайома нам інструкція constrained, а це дозволяє викликати методи на value типах без боксингу. Якщо ж метод працює і з value типами і з reference типами, то, наприклад, порівняння на null краще писати так:
 
 
if (!typeof(T).IsValueType && value == null)
	// Do something

 
Також існує проблема з оператором
as
. Типова практика відразу робити приведення за допомогою оператора
as
замість перевірки на тип і приведення до нього. Але у випадку, якщо у вас може бути value тип, то краще все-таки спочатку перевірити на тип, а потім привести, тому що оператор
as
працює тільки з reference типами, і відбудеться спочатку боксинг, а потім вже виклик
isinst
.
 
 
3. Виклики методів перерахувань
Перерахування до C # сильно засмучують. Проблема в тому, що будь-який виклик методу у перерахування викликає боксинг:
 
 
[Flags]
public enum Flags
{
	First = 1 << 0,
	Second = 1 << 1,
	Third = 1 << 2
}

public Boolean Foo(Flags flags)
{
	return flags.HasFlag(Flags.Second);
}

 
 
IL_0000: ldarg.1
IL_0001: box HabraTests.Flags
IL_0006: ldc.i4.2
IL_0007: box HabraTests.Flags
IL_000c: call instance bool [mscorlib]System.Enum::HasFlag(class [mscorlib]System.Enum)

 
Більше того, навіть метод GetHashCode () викликає боксинг. Тому якщо вам раптом потрібен хеш код від перерахування, то спочатку зробіть приведення до його underlying типу. А ще, якщо ви раптом використовуєте перерахування як ключ в Dictionary, то зробіть власний IEqualityComparer, інакше при кожному виклику GetHashCode () буде боксинг.
 
 
4. Перерахування до generic методах
Логічним продовженням пунктів 2 і 3 є бажання подивитися, а як поводитиметься перерахування в generic методі. З одного боку, якщо є реалізація методу у value типу, то generic методи вміють викликати методи інтерфейсів у структур без боксингу. З іншого боку, всі реалізації методів існують у базового класу
Enum
, а не у нами створених перерахувань. Напишемо невеликий тест, щоб зрозуміти, що відбувається всередині.
 
 Код тесту
public static void Main()
{
	Double intAverageGrow, enumAverageGrow;
	Int64 intMinGrow, intMaxGrow, enumMinGrow, enumMaxGrow;

	var result1 = Test<Int32>(() => GetUlong(10), out intAverageGrow, out intMinGrow, out intMaxGrow);
	var result2 = Test<Flags>(() => GetUlong(Flags.Second), out enumAverageGrow, out enumMinGrow, out enumMaxGrow);

	Console.WriteLine("Int32 memory change. Avg: {0}, Min: {1}, Max: {2}", intAverageGrow, intMinGrow, intMaxGrow);
	Console.WriteLine("Enum  memory change. Avg: {0}, Min: {1}, Max: {2}", enumAverageGrow, enumMinGrow, enumMaxGrow);

	Console.WriteLine(result1 + result2);
	Console.ReadKey(true);
}

public static UInt64 GetUlong<T>(T value)
	where T : struct, IConvertible
{
	return value.ToUInt64(CultureInfo.InvariantCulture);
}

public static UInt64 Test<T>(Func<UInt64> testedMethod, out Double averageGrow, out Int64 minGrow, out Int64 maxGrow)
{
	GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;

	var previousTotalMemory = GC.GetTotalMemory(false);
	Int64 growSum = 0;
	minGrow = 0;
	maxGrow = 0;

	UInt64 sum = 0;
	for (var i = 0; i < 100000; i++)
	{
		sum += testedMethod();

		var currentTotalMemory = GC.GetTotalMemory(false);
		var grow = currentTotalMemory - previousTotalMemory;
		growSum += grow;

		if (minGrow > grow)
			minGrow = grow;

		if (maxGrow < grow)
			maxGrow = grow;

		previousTotalMemory = currentTotalMemory;
	}

	averageGrow = growSum / 100000.0;

	return sum;
}

 
 
Результат:
 
 
Int32 memory change. Avg: 0, Min: 0, Max: 0
Enum  memory change. Avg: 3,16756, Min: -2079476, Max: 8192

 
Як ми бачимо, з перерахуваннями і тут все не слава богу: відбувається боксинг при кожному виклику методу ToUInt64 (). Але зате наочно видно, що виклик интерфейсного методу у Int32 не викликає ніякого боксингу.
 
А під кінець і почасти як висновок хочеться додати, що value типи здорово допомагають підняти продуктивність, але потрібно уважно стежити за тим, як вони використовуються, інакше в результаті боксингу головна їх перевага буде нівельовано.
У наступній статті мені хотілося б розповісти про місця, де неочевидним чином знаходяться глобальні точки синхронізації, і як їх обходити. Stay tuned.

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

0 коментарів

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