Привет, Хабр! Причинами написания этой статьи послужило жестокое задолбательство пониманием некоторых аспектов программирования на языке C# у моих коллег. Хотя нет, пожалуй пусть будет калек. Так точнее.

Дано: утверждение “использование var вместо явного указания типа ухудшает производительность, компилятор все приводит в object”.

Задача: удостовериться в истинности данного утверждения, абстрагироваться от физического насилия.

Как следует из руководства по языку C#:

Переменные, объявленные в области метода, могут иметь неявный "тип". var Неявно типизированная локальная переменная строго типизирована так, как если бы вы объявили тип самостоятельно, но компилятор определяет тип.

Также в руководстве указывается, что объявления

var a = 10; // Implicitly typed.
int b = 10; // Explicitly typed.

функционально эквивалентны.

Но что они понимают, писаки эти, в отличие от людей, имеющих *-цать лет опыта разработки!

Для пущего эффекта представим на время чтения статьи, что мы дурачки. Можно включить Моргенштерна на фоне.

Что нужно понять:

  1. Отличия в IL-коде

  2. Размещение в памяти

  3. Что же там "медленное"

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 и не пахнет.

Для полной картины приложу нотариально заверенный скриншот.

C# и IL-код декомпилированной сборки
C# и IL-код декомпилированной сборки

Мамкины скиловые шарписты тут же скажут что ничего-то я не понимаю в разработке. И вообще. Остается только прибавить громкости очередному треку Моргенштерна и отправиться исследовать память.

Размещение в памяти

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`ы. Если перейти в просмотр экземпляров, можно увидеть объекты, и их адреса, выданные ранее в окне интерпретации.

Снимок памяти процесса
Снимок памяти процесса
Адрес в памяти указывает на объект в куче
Адрес в памяти указывает на объект в куче

Что же из всего этого следует?

  1. Использование var и явное объявление типа дает один и тот же результат

  2. В памяти объекты хранятся по одним и тем же правилам. Значимые - в стеке, ссылочные - в куче. Никаких исключений, будь он объявлен как var, или Иван Федорович Крузенштерн.

Быстродействие

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

В свете вышеизложенного, единственный значимый в контексте быстродействия фактор, на который следует обратить самое пристальное внимание - это быстродействие уворачивания от зуботычины при высказывании мнения “var медленный”.

На этом исследования завершены, достаем шотган, принудительно (почти как kill -9) выключаем Моргенштерна. Плавно возвращаемся в ресурсное состояние.

P.S. Челлендж для мамкиных шарпистов - напишите статью о том, почему я не прав, с исследованием и пруфами. Скриншоты у нотариуса можно не заверять, он еще обедает.

Комментарии (7)


  1. BugM
    26.11.2022 19:57
    +5

    Просто открыть документацию и прочитать не проще было?

    https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/statements/declarations


  1. GbrtR
    26.11.2022 20:13

    Сам предпочитаю использовать явное указание типов когда можно, так как это улучшает «быстродействие» чтения кода.

    Но var конечно это не про быстродействие, оно идентично, а про компактность кода. Плюс анонимные классы, например когда какое-нибудь LINQ выражение возвращет новосозданные объекты.


  1. tempick
    26.11.2022 20:14
    +4

    Неприятно было читать, если честно


  1. Skykharkov
    26.11.2022 20:24

    Хороший разбор. Ну как для новичка и если доки не читать.
    Сам заморочился в свое время таким, для себя и вывод был точно таким-же. Но это не значит, что если вы объявили var a = 10; оно прямо вот во всех случаях будет идентично int a = 10; Тут уж как компилятор решит. Вполне возможно, что если дальше по коду у вас переменная "а" и как object куда-то типизируется и как char и как множество какого-то enum, или там Clone через ICloneable, то компилятор может подумать и решить - фиг с ним, пусть "a" будет "типа" object, (раз уж так этому кодеру хочется) а дальше разберемся. Но в подавляющем большинстве случаев, вы абсолютно правы, var - это норма (с)
    А так, если просто, тяжеловат стиль статьи...


    1. Politura
      26.11.2022 20:32
      +4

      компилятор может подумать и решить

      Нет, не может, т.к. это будет противоречить документации.


  1. ARad
    26.11.2022 20:28

    Эх, Пашка! Эту твою энергию, да в мирных целях… давно бы был женат!

    Бывают разные стили написания кода и вам надо про них почитать... А в команде желательно использовать одинакой стиль, или небольшие вариации одного стиля.

    И это не имеет никакого отношения к быстродействию.


  1. Akon32
    26.11.2022 20:55

    Скорее, "зашквар" - это полагаться на вывод дизассемблера в частном случае, если есть спецификация языка, чётко проясняющая этот вопрос в общем случае. И да, использование компилятора, который внезапно работает не по спецификации - ну вы поняли.