Привет, Хабр! Причинами написания этой статьи послужило жестокое задолбательство пониманием некоторых аспектов программирования на языке C# у моих коллег. Хотя нет, пожалуй пусть будет калек. Так точнее.
Дано: утверждение “использование var вместо явного указания типа ухудшает производительность, компилятор все приводит в object”.
Задача: удостовериться в истинности данного утверждения, абстрагироваться от физического насилия.
Как следует из руководства по языку C#:
Переменные, объявленные в области метода, могут иметь неявный "тип". var Неявно типизированная локальная переменная строго типизирована так, как если бы вы объявили тип самостоятельно, но компилятор определяет тип.
Также в руководстве указывается, что объявления
var a = 10; // Implicitly typed.
int b = 10; // Explicitly typed.
функционально эквивалентны.
Но что они понимают, писаки эти, в отличие от людей, имеющих *-цать лет опыта разработки!
Для пущего эффекта представим на время чтения статьи, что мы дурачки. Можно включить Моргенштерна на фоне.
Что нужно понять:
Отличия в IL-коде
Размещение в памяти
Что же там "медленное"
IL-код
Для примера возьмем значимый и ссылочный типы, объявим их через var и без.
int i = 45;
var i1 = 45;
object o = 45;
var o1 = (object)45;
List<string> l = new List<string>();
var l1 = new List<string>();
object ol = new List<string>();
var ol1 = new List<string>();
Собираем, открываем в dotPeek, видим в IL-коде следующую картину:
// [9 7 - 9 18]
IL_0001: ldc.i4.s 45 // 0x2d
IL_0003: stloc.0 // i
// [10 7 - 10 19]
IL_0004: ldc.i4.s 45 // 0x2d
IL_0006: stloc.1 // i1
…
// [15 7 - 15 21]
IL_0013: ldc.i4.s 45 // 0x2d
IL_0015: box [System.Runtime]System.Int32
IL_001a: stloc.s o
// [16 7 - 16 27]
IL_001c: ldc.i4.s 45 // 0x2d
IL_001e: box [System.Runtime]System.Int32
IL_0023: stloc.s o1
Для первых объявлений переменных i и i1 мы видим одни и те же опкоды ldc.i4.s и stloc, а для объявлений o и o1 через object добавляется box и stloc.s, что же это все значит?
Опкод ldc.i4.s помещает в стек значение с типом int (вообще он int8 помещает как int32, но занудствовать не хочется).
Опкод stloc извлекает верхнее значение стека и помещает его в список локальных переменных (.0 и .1 указывает на индекс в списке локальных переменных).
А вот для object уже добавилась упаковка первоначально размещенного значения, и опкод stloc.s, ну оно и понятно, тип-то ссылочный. Тот же опкод можно увидеть в листинге для типа List.
Как видим, объявление типа через int и аналогичный ему var сгенерировал одинаковый код, в котором object`ом при использовании var и не пахнет.
Для полной картины приложу нотариально заверенный скриншот.
Мамкины скиловые шарписты тут же скажут что ничего-то я не понимаю в разработке. И вообще. Остается только прибавить громкости очередному треку Моргенштерна и отправиться исследовать память.
Размещение в памяти
IL-код может и одинаковый, но что, если в памяти оно размещается по-другому? Например var размещается в куче, а не в стеке. Посмотрим-ка на не-IL код, ну и в память, ясное дело.
Откроем дизассемблированный код в отладчике Visual Studio.
9: int i = 45;
00007FFC03D90FE6 mov dword ptr [rbp+84h],2Dh
10: var i1 = 45;
00007FFC03D90FF0 mov dword ptr [rbp+80h],2Dh
11:
12: object o = 45;
00007FFC03D90FFA mov rcx,7FFC03D5B1F0h
00007FFC03D91004 call CORINFO_HELP_NEWSFAST (07FFC638B7840h)
…
13: var o1 = (object)45;
00007FFC03D91020 mov rcx,7FFC03D5B1F0h
00007FFC03D9102A call CORINFO_HELP_NEWSFAST (07FFC638B7840h)
Так-с, что мы тут видим? Для var и int код, исполняемый процессором, идентичен, в память по адресу в регистре RBP, с некоторым смещением, помещается значение 2D (45 в десятичной системе счисления). Тип dword говорит нам, что это 4 байта. Все верно, тип int это 4 байта.
Посмотрим в память! Для этого в окне интерпретации введем &i. Выведется адрес, по которому размещено значение переменной. То же самое для i1. Кстати, в листинге выше видно, что значения размещены друг за другом, сначала i1, через 4 байта i.
Перейдя на адрес переменной i1 видим:
0x0000007133B7E5E0 2d 00 00 00 2d 00 00 00
Наши родненькие байты, лежат, скучают. А что там у object? Вводим в окне интерпретации &o и &o1. Как видно из листинга, размещение переменных типа object работает по-другому, через вызов CORINFO_HELP_NEWSFAST.
0x0000007133B7E5D0 b8 ac 0f cc 13 02 00 00
В памяти ничего похожего на 2D не обнаружилось. Хранимое для object значение является адресом объекта в куче. Сделаем снимок памяти, посмотрим, что же у нас есть в куче! А в куче, извините за тавтологию, куча объектов. Нас интересуют Int`ы. Если перейти в просмотр экземпляров, можно увидеть объекты, и их адреса, выданные ранее в окне интерпретации.
Что же из всего этого следует?
Использование var и явное объявление типа дает один и тот же результат
В памяти объекты хранятся по одним и тем же правилам. Значимые - в стеке, ссылочные - в куче. Никаких исключений, будь он объявлен как var, или Иван Федорович Крузенштерн.
Быстродействие
Не вижу большого смысла проводить какие-то исследования в части быстродействия, поскольку размещение и типизация абсолютно идентичны, из чего я делаю вывод, что операции с типами в обоих случаях по скорости будут эквивалентны.
В свете вышеизложенного, единственный значимый в контексте быстродействия фактор, на который следует обратить самое пристальное внимание - это быстродействие уворачивания от зуботычины при высказывании мнения “var медленный”.
На этом исследования завершены, достаем шотган, принудительно (почти как kill -9) выключаем Моргенштерна. Плавно возвращаемся в ресурсное состояние.
P.S. Челлендж для мамкиных шарпистов - напишите статью о том, почему я не прав, с исследованием и пруфами. Скриншоты у нотариуса можно не заверять, он еще обедает.
Комментарии (7)
GbrtR
26.11.2022 20:13Сам предпочитаю использовать явное указание типов когда можно, так как это улучшает «быстродействие» чтения кода.
Но var конечно это не про быстродействие, оно идентично, а про компактность кода. Плюс анонимные классы, например когда какое-нибудь LINQ выражение возвращет новосозданные объекты.
Skykharkov
26.11.2022 20:24Хороший разбор. Ну как для новичка и если доки не читать.
Сам заморочился в свое время таким, для себя и вывод был точно таким-же. Но это не значит, что если вы объявилиvar a = 10;
оно прямо вот во всех случаях будет идентичноint a = 10;
Тут уж как компилятор решит. Вполне возможно, что если дальше по коду у вас переменная "а" и какobject
куда-то типизируется и какchar
и как множество какого-тоenum
, или там Clone через ICloneable, то компилятор может подумать и решить - фиг с ним, пусть "a" будет "типа"object
, (раз уж так этому кодеру хочется) а дальше разберемся. Но в подавляющем большинстве случаев, вы абсолютно правы, var - это норма (с)
А так, если просто, тяжеловат стиль статьи...Politura
26.11.2022 20:32+4компилятор может подумать и решить
Нет, не может, т.к. это будет противоречить документации.
ARad
26.11.2022 20:28Эх, Пашка! Эту твою энергию, да в мирных целях… давно бы был женат!
Бывают разные стили написания кода и вам надо про них почитать... А в команде желательно использовать одинакой стиль, или небольшие вариации одного стиля.
И это не имеет никакого отношения к быстродействию.
Akon32
26.11.2022 20:55Скорее, "зашквар" - это полагаться на вывод дизассемблера в частном случае, если есть спецификация языка, чётко проясняющая этот вопрос в общем случае. И да, использование компилятора, который внезапно работает не по спецификации - ну вы поняли.
BugM
Просто открыть документацию и прочитать не проще было?
https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/statements/declarations