Опубликовано по просьбе Rextor'а, у которого отсутствует аккаунт на хабаре.
Написать этот пост меня заставило обсуждение в посте Коварный вопрос по Event \ Delegate()
значения очередности инкремента (что лучше i++ или, ++i ), и имеет ли она принципиальное значение (количество порождаемых объектов и тактов выполнения).
Сразу предупрежу что я пишу именно насчет отдельно стоящих инкрементов, а не аргументов вызова метода.
Конструкция Метод(++i); конечно всегда будет лучше, чем i++; Метод(i);
Для того чтобы это выяснить было создано простейшее приложение проверяющее это, которое было откомпилировано как релиз(чтобы компилятор не добавил лишние точки останова) и разобрана Рефлектором Рефлектором.
По причине того, что в .net есть только 2 основных типа из которых создаются все остальные (ValueType и Object) было выбрано для рассмотрения 2 типа: Int32 и класс имеющий перегрузку инкремента. Смею предположить, что поведение всех остальных типов и способов перегрузки должно быть таким же.
И так листинг самого приложения:
Класс основного метода:
Класс с перегрузкой инкремента:
Откомпиленная программка была отправлена на растерзание Reflector’у (можно было конечно и через ILDasm пропустить, но я считаю работу с ним не удобной).
Из Reflector’а был получен IL код рассмотренный ниже (ссылка на файл с полным исходным кодом приведена в конце поста):
Первой строкой указывается, что с этого метода производится вход в приложение:
Указание размера используемого в методе стека:
И объявление всех переменных
Следующая строка загружает и выводит текст: «Почти что без полезный вывод текста», причина этого вывода будет раскрыта чуть ниже
Далее начинается сам текст метода, первая часть которого выполняет операции с интом:
Обнуление значений переменных
Вот и наши инкременты
Как видно из предыдущего участка кода инкремент обоих переменных в данном случае происходит одинаково
Следующий участок кода выводит значения переменных на консоль:
Первое различие появляется далее, при инкременте прямо в момент вывода на консоль (я немножко соврал когда в начале написал что этот случай не будет рассматриваться)
Далее идет часть метода выполняющая тоже самое с классами (честно говоря я даже не мог предположить что код будет настолько похож)
А вот сейчас, как и обещал раскрою причину того, зачем была первая строка выводящая на консоль текст. Дело в том, что я решил проверить а есть ли отличия после компиляции программы(в чистые машинные команды), но так как код инициализации метода и объявления сливался, пришлось вставить строку, разделяющую код метода и инициализации.
Так вот, в результате сравнения оказалось что код на чистом асме тоже почти не отличается:
При работе с интеджерами блоки кода копирующие значение в стек, производящие прибавление единицы и сохраняющие обратно были заменены на один оператор Inc.
В слечае с инкрементом класса меняется очередность вызова(как и в IL коде)
Исходники асма кстати тоже приведены ниже, желающие покопаться в них милости просим:
IL код
Ассемблерный код
Заархивированный проект
Написать этот пост меня заставило обсуждение в посте Коварный вопрос по 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 код
Ассемблерный код
Заархивированный проект