Pull to refresh

Все знают что "++i + ++i" — плохо, но что-же за ширмой?

Abnormal programming *
Баг, выглядывающий из-за ширмы на вас о_ОНесомненно, все программисты знают что использование выражений, подобных тому что приведено в заглавии поста, не то что нежелательно, а строго противопоказано. Такие конструкции, поведение компилятора в которых не определено, могут принести множество трудноуловимых ошибок и нежелательных последствий. Но уверен, многим начинающим программистам хотелось бы по глубже понять эту проблему и, заглянув за ширму компилятора, узнать что же именно происходит в таких случаях. Изучению одного из примеров подобного кода я и посвящаю этот пост. Добро пожаловать под кат :)

Объект исследования


Для примера, я разберу работу выражения "++i + ++i" на двух различных языках: С и С#. Первый, как известно, компилируется в нативный код процессора, а второй, если грубо, работает на основе виртуальной стековой машины. И так, рассмотрим сами примеры:

Исходник на Си:
  1. #include <stdio.h>
  2.  
  3. void main()
  4. {
  5.  int i = 5;
  6.  i = ++i + ++i;
  7.  printf("%d\n",i);
  8. }

И исходник на C#:
  1. using System;
  2.  
  3. public class Test
  4. {
  5.  public static void Main()
  6.  {
  7.   int i = 5;
  8.   i = ++i + ++i;
  9.   Console.WriteLine(i);
  10.  }
  11. }

За ширмой Си


Воспользовавшись дизассемблером, посмотрим что-то же в итоге сгенерировал компилятор Си:

#A#5:  int i = 5;                        
  cs:0295 BE0500         mov    si,0005  
#A#6:  i = ++i + ++i;                    
  cs:0298 46             inc    si       
  cs:0299 46             inc    si       
  cs:029A 8BC6           mov    ax,si    
  cs:029C 03C6           add    ax,si    
  cs:029E 8BF0           mov    si,ax    
#A#7:  printf("%d\n",i);                 
  cs:02A0 56             push   si       
  cs:02A1 B8AA00         mov    ax,00AA  
  cs:02A4 50             push   ax       
  cs:02A5 E8330C         call   _printf  

Как видно из листинга, компилятор отобразил переменную i на регистр SI процессора x86. После чего, дважды инкрементировав этот регистр, прибавил его самого к себе через аккумулятор AX. В результате, переменная i становится равной 14.

За ширмой C#


С помощью Ildasm посмотрим что скрывается за C#:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       21 (0x15)
  .maxstack  3
  .locals init (int32 V_0)
  IL_0000:  ldc.i4.5    // push 5                       5
  IL_0001:  stloc.0     // i := pop()                   null
  IL_0002:  ldloc.0     // push i                       i
  IL_0003:  ldc.i4.1    // push 1                       i, 1
  IL_0004:  add         // push (pop() + pop())         (i+1) т.е. 6
  IL_0005:  dup         // copy вершины стека           6, 6
  IL_0006:  stloc.0     // i := pop() // i := 6         6
  IL_0007:  ldloc.0     // push i                       6, i
  IL_0008:  ldc.i4.1    // push 1                       6, i, 1
  IL_0009:  add         // push (pop() + pop())         6, (i+1) т.е. 7
  IL_000a:  dup         // copy вершины стека           6, 7, 7
  IL_000b:  stloc.0     // i := pop() // i := 7         6, 7
  IL_000c:  add         // push (pop() + pop())         13
  IL_000d:  stloc.0     // i := pop()                   null
  IL_000e:  ldloc.0     // push i                       i
  IL_000f:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0014:  ret
} // end of method Test::Main

Для наглядности, я добавил комментарии к ассемблеру виртуальной стековой машины. Смотря на листинг, можно увидеть что для инкремента переменной i в стек помещается сама переменная и единица. Затем выполняется команда сложения которая, взяв два значения из стека и сложив, помещает результат опять в стек. Затем происходит дублирование вершины стека и запись значения обратно в переменную. Таким образом в стеке остается 5 + 1, т.е. 6. Далее цикл повторяется для другого инкремена: заносится в стек переменная, вслед за ней — единица, происходит сложение, дублирование вершины и запись результата второго инкремента назад в переменную. Теперь уже в i будет 7, а в стеке останутся 6 от первого случая и 7 от второго. Затем выполняется команда сложения и результат, равный теперь 13, заносится в переменную.

Итоги


Вот такой вот, с виду одинаковый, код выполняется совершенно по разному в различных условиях. Избегайте подобного кода в своих программах.

Если тема вам показалась интересной — дайте знать, и я напишу еще пару занятных моментов из мира компиляции ;)

UPD. В комментариях подсказывают что в РНР, Java, Actionscript 3, JavaScript и TCL результат тоже равен 13, а вот в Perl, K++ и C GCC — 14 (но результат GCC может зависеть от настроек оптимизатора) :)

Отличились PL/SQL и Python — 10 (как подсказывает К.О. — из-за отсутствия инкрементов в языке), однако Bash — 12.

Так же есть компиляторы, непозволяющие написание подобного кода. Например Ruby (или вот так).

А каким будет результат в твоем, %username%, компиляторе?

UPD2. Ссылки по теме: точки следования Википедия, Алёна C++.
Tags:
Hubs:
Total votes 118: ↑100 and ↓18 +82
Views 22K
Comments Comments 85