Pull to refresh

Инкремент в C# изнутри

Reading time7 min
Views4.6K
Опубликовано по просьбе Rextor'а, у которого отсутствует аккаунт на хабаре.
Написать этот пост меня заставило обсуждение в посте Коварный вопрос по Event \ Delegate()
значения очередности инкремента (что лучше i++ или, ++i ), и имеет ли она принципиальное значение (количество порождаемых объектов и тактов выполнения).
Сразу предупрежу что я пишу именно насчет отдельно стоящих инкрементов, а не аргументов вызова метода.
Конструкция Метод(++i); конечно всегда будет лучше, чем i++; Метод(i);

Для того чтобы это выяснить было создано простейшее приложение проверяющее это, которое было откомпилировано как релиз(чтобы компилятор не добавил лишние точки останова) и разобрана Рефлектором Рефлектором.
По причине того, что в .net есть только 2 основных типа из которых создаются все остальные (ValueType и Object) было выбрано для рассмотрения 2 типа: Int32 и класс имеющий перегрузку инкремента. Смею предположить, что поведение всех остальных типов и способов перегрузки должно быть таким же.
И так листинг самого приложения:

Класс основного метода:

class Program{
    static void Main(string[] args){
      Console.WriteLine ("Почти что без полезный вывод текста");
      int i = 0;
      int j = 0;
      i++;
      ++j;
      Console.WriteLine(i);
      Console.WriteLine(j);
      Console.WriteLine(i++);
    // использован для того чтобы хитрый компилятор не удалил последний инкремент как ненужный
      Console.WriteLine(i);
      Console.WriteLine(++j);

      SampleClass class_i= new SampleClass();
      SampleClass class_j = new SampleClass();

      class_i++;
      ++class_j;
      Console.WriteLine(class_i);
      Console.WriteLine(class_j);

      Console.WriteLine(class_i++);
//аналогичная ситуация с инкрементом
      Console.WriteLine(class_i);
      Console.WriteLine(++class_j);

      Console.ReadLine();

    }
}

* This source code was highlighted with Source Code Highlighter.

Класс с перегрузкой инкремента:
    class SampleClass{
      public SampleClass() {
        num = 0;
      }
      int num;
      // перегрузка ++
      public static SampleClass operator ++(SampleClass a){
        return new SampleClass() {num=++a.num};
      }
      public override string ToString(){
        return num.ToString();
      }
    }

* This source code was highlighted with Source Code Highlighter.

Откомпиленная программка была отправлена на растерзание Reflector’у (можно было конечно и через ILDasm пропустить, но я считаю работу с ним не удобной).
Из Reflector’а был получен IL код рассмотренный ниже (ссылка на файл с полным исходным кодом приведена в конце поста):

Первой строкой указывается, что с этого метода производится вход в приложение:

  .entrypoint

Указание размера используемого в методе стека:
  .maxstack 3

И объявление всех переменных
.locals init (
    [0] int32 i,
    [1] int32 j,
    [2] class SampleClass class_i, //здесь и далее путь к классу сокращен для удобства чтения и нервов читателей
    [3] class SampleClass class_j
)

Следующая строка загружает и выводит текст: «Почти что без полезный вывод текста», причина этого вывода будет раскрыта чуть ниже
  L_0000: ldstr "\u0411\u0435\u0437\u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439 \u0442\u0435\u043a\u0441\u0442"
  L_0005: call void [mscorlib]System.Console::WriteLine(string)

Далее начинается сам текст метода, первая часть которого выполняет операции с интом:

Обнуление значений переменных
  //i=0
  L_000a: ldc.i4.0 //заносим в стек 0
  L_000b: stloc.0 // сохраняем его в i

  //j=0;
  L_000c: ldc.i4.0 //заносим в стек 0
  L_000d: stloc.1 // сохраняем его в j

Вот и наши инкременты
  //i++
  L_000e: ldloc.0 //заносим в стек i
  L_000f: ldc.i4.1 //заносим в стек 1
  L_0010: add   //суммируем их
  L_0011: stloc.0 // сохраняем значение из стека в i

  //++j
  L_0012: ldloc.1 //аналогично предыдущему
  L_0013: ldc.i4.1
  L_0014: add
  L_0015: stloc.1

* This source code was highlighted with Source Code Highlighter.

Как видно из предыдущего участка кода инкремент обоих переменных в данном случае происходит одинаково

Следующий участок кода выводит значения переменных на консоль:

  // загрузка и вывод сначала i потом j
  L_0016: ldloc.0 //
  L_0017: call void [mscorlib]System.Console::WriteLine(int32)
  L_001c: ldloc.1
  L_001d: call void [mscorlib]System.Console::WriteLine(int32)

Первое различие появляется далее, при инкременте прямо в момент вывода на консоль (я немножко соврал когда в начале написал что этот случай не будет рассматриваться)
  L_0022: ldloc.0   // загружаем i в стек
  L_0023: dup     // помещаем в стек копию первого элемента
  L_0024: ldc.i4.1   // загружаем 1 в стек
  L_0025: add     // складывается 2 ближайших элемента из стека
  L_0026: stloc.0   // сохранение первого элемента из стека в i

  L_0027: call void [mscorlib]System.Console::WriteLine(int32) //в стеке еще один элемент его и выводим

  // вывод копии i призванной для того чтобы последняя операция сложения (024-026) не очистилась как ненужная
  L_002c: ldloc.0  
  L_002d: call void [mscorlib]System.Console::WriteLine(int32)

  // вывод j++ (аналогичен выводу i за исключением очереди копирования переменной в массиве)
  L_0032: ldloc.1
  L_0033: ldc.i4.1
  L_0034: add
  L_0035: dup     //а вот какраз и различие, оно в том что в блоке 22-27 инструкция копирования в стеке стоит перед инструкциями сложения
  L_0036: stloc.1
  L_0037: call void [mscorlib]System.Console::WriteLine(int32)

* This source code was highlighted with Source Code Highlighter.

Далее идет часть метода выполняющая тоже самое с классами (честно говоря я даже не мог предположить что код будет настолько похож)
  //создаем новый обьект SampleClass (class_i)
  L_003c: newobj instance void SampleClass::.ctor()
  L_0041: stloc.2

  //создаем новый обьект SampleClass (class_j)
  L_0042: newobj instance void SampleClass::.ctor()
  L_0047: stloc.3

  // загрузка в стек указателя на class_i вызов ++ и сохранение результата
  L_0048: ldloc.2
  L_0049: call class SampleClass SampleClass::op_Increment(class SampleClass)
  L_004e: stloc.2

  // аналогичное действие с class_j
  L_004f: ldloc.3
  L_0050: call class SampleClass SampleClass::op_Increment(class SampleClass)
  L_0055: stloc.3

  // вывод значения class_i
  L_0056: ldloc.2
  L_0057: call void [mscorlib]System.Console::WriteLine(object)

  // вывод значения class_j
  L_005c: ldloc.3
  L_005d: call void [mscorlib]System.Console::WriteLine(object)

  // загрузка класса class_i в стек, копирование его в стеке вывод на консоль и сохранение переменной
  L_0062: ldloc.2
  L_0063: dup
  L_0064: call class SampleClass SampleClass::op_Increment(class SampleClass)
  L_0069: stloc.2
  
  // вывод результата сложения
  L_006a: call void [mscorlib]System.Console::WriteLine(object)
  L_006f: ldloc.2
  // опять же вывод, предназначенный, для того чтобы L_0063: dup выполнился а не был удален компилятором как ненужный
  L_0070: call void [mscorlib]System.Console::WriteLine(object)
  
  // аналогичное действие с class_j за исключением того, что выводится он сразу
  L_0075: ldloc.3
  L_0076: call class SampleClass SampleClass::op_Increment(class SampleClass)
  L_007b: dup
  L_007c: stloc.3
  L_007d: call void [mscorlib]System.Console::WriteLine(object)
  
  // "Pres any Key to Contune.. ;)"
  L_0082: call string [mscorlib]System.Console::ReadLine()
  L_0087: pop //получение из стека резульатат выполнения метода
  L_0088: ret //выход

* This source code was highlighted with Source Code Highlighter.

А вот сейчас, как и обещал раскрою причину того, зачем была первая строка выводящая на консоль текст. Дело в том, что я решил проверить а есть ли отличия после компиляции программы(в чистые машинные команды), но так как код инициализации метода и объявления сливался, пришлось вставить строку, разделяющую код метода и инициализации.
Так вот, в результате сравнения оказалось что код на чистом асме тоже почти не отличается:
При работе с интеджерами блоки кода копирующие значение в стек, производящие прибавление единицы и сохраняющие обратно были заменены на один оператор Inc.
В слечае с инкрементом класса меняется очередность вызова(как и в IL коде)
Исходники асма кстати тоже приведены ниже, желающие покопаться в них милости просим:

IL код

Ассемблерный код

Заархивированный проект
Tags:
Hubs:
+7
Comments2

Articles