Доброго дня, Хабр!
Многие неопытные разработчики не всегда знают и понимают, что же происходит за кулисами их кода. Сейчас речь пойдет об упаковке и распаковке значимых типов данных (по-русски это звучит ужасно, поэтому «boxing and unboxing value types»).
Под катом небольшой пример и измерение времени выполнения.
Что такое упаковка (boxing)?
Коротко. Есть значимые типы данных (value types) и ссылочные (reference types). Переменные значимых типов данных хранят само значение (спасибо, кэп!), переменные ссылочных типов данных — ссылку на участок в памяти, где хранится это значение.
int valType = 15;
Это значимый тип данных. Значение переменной valType будет храниться в стэке. Многие стандартные типы данных — значимые (int, byte, long, bool и т.д.).
Дальше если мы попробуем сделать вот так:
int valType = 15; Object refType = valType;
И получим в результате переменную ссылочного типа (refType). Тут произойдет следующее: сначала в стэке появится значение переменной valType (значимый тип), потом в памяти будет создан контейнер для хранения значения этой переменной (в нашем случае контейнер для переменной типа int, то есть 4 байта под значение + sync block index (еще 4 байта)), а вот уже указатель на этот контейнер и будет храниться в нашей переменной ссылочного типа (refType). Этот процесс называется упаковка (boxing).
Подробности можно посмотреть тут, а лучше прочитать в книжке Дж.Рихтера «CLR via C#» (Глава 5).
Самое неприятное в этих операциях то, что они происходят неявно.
Например, мы хотим вывести число в консоль. Так:
Console.WriteLine(20);
Или вот так:
Console.WriteLine("{0}", 20);
В чем разница? Давайте посмотрим на результат компиляции в MSIL (сделать это можно утилитой ILdasm.exe):
// Это для вызова Console.WriteLine(20); IL_0000: ldc.i4.s 20 IL_0002: call void [mscorlib]System.Console::WriteLine(int32) // А это для вызова Console.WriteLine("{0}", 20); IL_0007: ldstr "{0}" IL_000c: ldc.i4.s 20 IL_000e: box [mscorlib]System.Int32 IL_0013: call void [mscorlib]System.Console::WriteLine(string, object)
Во втором случае мы видим команду box, которая и выполняет упаковку.
Чтобы понять откуда она взялась, взглянем на сигнатуру метода Console.WriteLine и заметим, что их есть аж 18 штук.
В первом вызове используется такая сигнатура:
void WriteLine(int value);
Тут все просто — этот метод принимает значимый тип данных int, передаем мы значение типа int, происходит передача параметра по значению. Упаковка тут не нужна.
Во втором случае используется другая сигнатура:
void WriteLine(string format, object arg0);
С передачей форматной строки понятно: требуется строка — мы передаем строку. А с аргументом arg0 чуть сложнее: метод просит от нас объект ссылочного типа данных object, а передаем мы в метод значение типа int. Вот тут и нужна упаковка. В результате чего в памяти создается контейнер для типа int, в него копируется значение 20, и указатель на этот контейнер попадает в аргумент arg0.
Теперь попробуем посчитать, сильно ли замедляют данные операции наш код.
Для этого я написал небольшой кусочек кода:
static void Main() { // переменная значимого типа, неупакованная var val = 15; // переменная ссылочного типа, уже упакованная Object obj = val; // количество циклов - мильён =) const int cycles = 1000000; var str = ""; // временные результаты будем заносить в список var results = new List<TimeSpan>(); // повторяем опыт 20 раз, чтобы получить более достоверное среднее значение for (var j = 0; j < 20; j++) { // засекаем время var start = DateTime.Now; for (var i = 0; i < cycles; i++) { // создаем строку из 10 одинаковых чисел // тут передаются уже упакованные значения str = String.Format("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}", obj, obj, obj, obj, obj, obj, obj, obj, obj, obj); } // останавливаем таймер var end = DateTime.Now; // и получаем время выполнения цикла без операций упаковки (box) var objResult = end.Subtract(start); // снова засекаем время start = DateTime.Now; for (var i = 0; i < cycles; i++) { // делаем точно такую же строку // но за этот вызов происходит 10 операций упаковки (box) str = String.Format("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}", val, val, val, val, val, val, val, val, val, val); } // останавливаем таймер end = DateTime.Now; // и получаем время выполнения цикла, в котором было 10 миллионов операций упаковки var valResult = end.Subtract(start); // разницу во времени между двумя циклами аккуратно заносим в список results.Add(valResult.Subtract(objResult)); } // выводим список результатов foreach (var timeDif in results) { Console.WriteLine(timeDif); } // и считаем среднюю разницу Console.WriteLine(); Console.Write("Milliseconds need for 10KK boxing operations: "); Console.WriteLine(results.Aggregate(TimeSpan.Zero, (sum, current) => sum.Add(current)).TotalMilliseconds / results.Count); }
Результат выполнения я получил следующий (процессор Intel Core i5 750 2.67GHz, 4 ядра, выполнялось на одном):
00:00:00.0600060 00:00:00.0770077 00:00:00.0570057 00:00:00.0710071 00:00:00.0680068 00:00:00.0650065 00:00:00.0530053 00:00:00.0740074 00:00:00.0570057 00:00:00.0580058 00:00:00.0590059 00:00:00.0500050 00:00:00.0550055 00:00:00.0720072 00:00:00.0800080 00:00:00.0640064 00:00:00.0640064 00:00:00.0670067 00:00:00.0660066 00:00:00.0590059 Milliseconds need for 10KK boxing operations: 63,80638
Итого в среднем почти 64мс на 10млн. операций упаковки.
Вывод
В качестве вывода хочу сказать, что все вышеизложенное вовсе не повод параноидально выискивать дизассемблером boxing’и в своем коде и добиваться лишнего миллиметра в секунду на скорости сто километров в час. Нет, конечно, это полный бред. Но понимать, что на самом деле происходит в вашем коде, важно. И в какой-то момент лишняя операция в цикле, выполняемом миллиарды раз, может стать критичной.
ссылка на оригинал статьи http://habrahabr.ru/post/210108/