Идентификация математических операторов
"…если вы обессилены, то не удивительно, что вся ваша жизнь -- не развлечение. У вас… так много вычислений, расчетов, которые необходимо сделать в вашей жизни, что она просто не может быть развлечением."
Ошо "Пустая Лодка"
Беседы по высказываниям Чжуан Цзы
Идентификация оператора "+". В общем случае оператор "+" транслируется либо в машинную инструкцию ADD, "перемалывающую" целочисленные операнды, либо в инструкцию FADDx, обрабатывающую вещественные значения. Оптимизирующие компиляторы могут заменять "ADD xxx, 1" более компактной командой "INC xxx", а конструкцию "c = a + b + const" транслировать в машинную инструкцию "LEA c, [a + b + const]". Такой трюк позволяет одним махом складывать несколько переменных, возвратив полученную сумму в любом регистре общего назначения, – не обязательно в левом слагаемом как это требует мнемоника команды ADD. Однако, "LEA" не может быть непосредственно декомпилирована в оператор "+", поскольку она используется не только для оптимизированного сложения (что, в общем-то, побочный продукт ее деятельности), но и по своему непосредственному назначению – вычислению эффективного смещения. (подробнее об этом см. "Идентификация констант и смещений", "Идентификация типов"). Рассмотрим следующий пример:
main()
{
int a, b,c;
c = a + b;
printf("%x\n",c);
c=c+1;
printf("%x\n",c);
}
Листинг 204 Демонстрация оператора "+"
Результат его компиляции компилятором Microsoft Visual C++ 6.0 с настройками по умолчанию должен выглядеть так:
main proc near ; CODE XREF: start+AFp
var_c = dword ptr -0Ch
var_b = dword ptr -8
var_a = dword ptr -4
push ebp
mov ebp, esp
; Открываем кадр стека
sub esp, 0Ch
; Резервируем память для локальных переменных
mov eax, [ebp+var_a]
; Загружаем в EAX значение переменной var_a
add eax, [ebp+var_b]
; Складываем EAX со значением переменной var_b
и записываем результат в EAX
mov [ebp+var_c], eax
; Копируем сумму var_a
и var_b в переменную var_c, следовательно:
; var_c = var_a + var_b
mov ecx, [ebp+var_c]
push ecx
push offset asc_406030 ; "%x\n"
call _printf
add esp, 8
; printf("%x\n", var_c)
mov edx, [ebp+var_c]
; Загружаем в EDX значение переменной var_c
add edx, 1
; Складываем EDX со значением 0х1, записывая результат в EDX
mov [ebp+var_c], edx
; Обновляем var_c
; var_c = var_c +1
mov eax, [ebp+var_c]
push eax
push offset asc_406034 ; "%x\n"
call _printf
add esp, 8
; printf("%\n",var_c)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 205
А теперь посмотрим, как будет выглядеть тот же самый пример, скомпилированный с ключом "/Ox" (максимальная оптимизация):
main proc near ; CODE XREF: start+AFp
push ecx
; Резервируем место для одной локальной переменной
; (компилятор посчитал, что три переменные можно ужать в одну и это дейст. так)
mov eax, [esp+0]
; Загружаем в EAX значение переменной var_a
mov ecx, [esp+0]
; Загружаем в EAX значение переменной var_b
; (т.к .переменная не инициализирована загружать можно откуда угодно)
push esi
; Сохраняем регистр ESI в стеке
lea esi, [ecx+eax]
; Используем LEA для быстрого сложения ECX и EAX с последующей записью суммы
; в регистр ESI
; "Быстрое сложение" следует понимать не в смысле, что команда LEA выполняется
; быстрее чем ADD, - количество тактов той и другой одинаково, но LEA
; позволяет избавиться от создания временной переменной для сохранения
; промежуточного результата сложения, сразу направляя результат в ESI
; Таким образом, эта команда декомпилируется как
; reg_ESI = var_a + var_b
push esi
push offset asc_406030 ; "%x\n"
call _printf
; printf("%x\n", reg_ESI)
inc esi
; Увеличиваем ESI на единицу
; reg_ESI = reg_ESI + 1
push esi
push offset asc_406034 ; "%x\n"
call _printf
add esp, 10h
; printf("%x\n", reg_ESI)
pop esi
pop ecx
retn
main endp
Листинг 206
Остальные компиляторы (Borland C++, WATCOM C) генерируют приблизительно идентичный код, поэтому, приводить результаты бессмысленно – никаких новых "изюминок" они в себе не несут.
Идентификация оператора "–". В общем случае оператор "– " транслируется либо в машинную инструкцию SUB
(если операнды – целочисленные значения), либо в инструкцию FSUBx (если операнды – вещественные значения). Оптимизирующие компиляторы могут заменять "SUB xxx, 1" более компактной командой "DEC xxx", а конструкцию "SUB a, const" транслировать в "ADD a, -const", которая ничуть не компактнее и ни сколь не быстрей (и та, и другая укладываться в один так), однако, хозяин (компилятор) – барин. Покажем это на следующем примере:
main()
{
int a,b,c;
c = a - b;
printf("%x\n",c);
c = c - 10;
printf("%x\n",c);
}
Листинг 207 Демонстрация идентификации оператора "-"
Не оптимизированный вариант будет выглядеть приблизительно так:
main proc near ; CODE XREF: start+AFp
var_c = dword ptr -0Ch
var_b = dword ptr -8
var_a = dword ptr -4
push ebp
mov ebp, esp
; Открываем кадр стека
sub esp, 0Ch
; Резервируем память под локальные переменные
mov eax, [ebp+var_a]
; Загружаем в EAX значение переменной var_a
sub eax, [ebp+var_b]
; Вычитаем из var_a
значением переменной var_b, записывая результат в EAX
mov [ebp+var_c], eax
; Записываем в var_c
разность var_a и var_b
; var_c = var_a – var_b
mov ecx, [ebp+var_c]
push ecx
push offset asc_406030 ; "%x\n"
call _printf
add esp, 8
; printf("%x\n", var_c)
mov edx, [ebp+var_c]
; Загружаем в EDX значение переменной var_c
sub edx, 0Ah
; Вычитаем из var_c
значение 0xA, записывая результат в EDX
mov [ebp+var_c], edx
; Обновляем var_c
; var_c = var_c – 0xA
mov eax, [ebp+var_c]
push eax
push offset asc_406034 ; "%x\n"
call _printf
add esp, 8
; printf("%x\n",var_c)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 208
А теперь рассмотрим оптимизированный вариант того же примера:
main proc near ; CODE XREF: start+AFp
push ecx
; Резервируем место для локальной переменной var_a
mov eax, [esp+var_a]
; Загружаем в EAX значение локальной переменной var_a
push esi
; Резервируем место для локальной переменной var_b
mov esi, [esp+var_b]
; Загружаем в ESI значение переменной var_b
sub esi, eax
; Вычитаем из var_a
значение var_b, записывая результат в ESI
push esi
push offset asc_406030 ; "%x\n"
call _printf
; printf("%x\n", var_a – var_b)
add esi, 0FFFFFFF6h
; Добавляем
к ESI (разности var_a и
var_b) значение 0хFFFFFFF6
; Поскольку, 0xFFFFFFF6 == -0xA, данная строка кода выглядит так:
; ESI = (var_a – var_b) + (– 0xA) = (var_a – var_b) – 0xA
push esi
push offset asc_406034 ; "%x\n"
call _printf
add esp, 10h
; printf("%x\n", var_a – var_b – 0xA)
pop esi
pop ecx
; Закрываем кадр стека
retn
main endp
Листинг 209
Остальные компиляторы (Borland, WATCOM) генерируют практически идентичный код, поэтому здесь не рассматриваются.
Идентификация оператора "/". В общем случае оператор "/" транслируется либо в машинную инструкцию "DIV" (беззнаковое целочисленное деление), либо в "IDIV" (целочисленное деление со знаком), либо в "FDIVx" (вещественное деление).
Если делитель кратен степени двойки, то "DIV" заменяется на более быстродействующую инструкцию битового сдвига вправо "SHR a, N", где a – делимое, а N – показатель степени с основанием два.
Несколько сложнее происходит быстрое деление знаковых чисел. Совершенно недостаточно выполнить арифметический сдвиг вправо (команда арифметического сдвига вправо SAR
заполняет старшие биты с учетом знака числа), ведь если модуль делимого меньше модуля делителя, то арифметический сдвиг вправо сбросит все значащие биты в "битовую корзину", в результате чего получиться 0xFFFFFFFF, т.е. –1, в то время как правильный ответ – ноль. Вообще же, деление знаковых чисел арифметическим сдвигом вправо дает округление в большую сторону, что совсем не входит в наши планы. Для округления знаковых чисел в меньшую сторону необходимо перед выполнением сдвига добавить к делимому число , где N
– количество битов, на которые сдвигается число при делении. Легко видеть, что это приводит к увеличению всех сдвигаемых битов на единицу и переносу в старший разряд, если хотя бы один из них не равен нулю.
Следует отметить: деление очень медленная операция, гораздо более медленная чем умножение (выполнение DIV
может занять свыше 40 тактов, в то время как MUL обычно укладываться в 4), поэтому, продвинутые оптимизирующие компиляторы заменяют деление умножением. Существует множество формул подобных преобразований, вот, например, она (самая популярная из них):
, где N – разрядность числа. Выходит, грань между умножением и делением очень тока, а их идентификация довольно сложна. Рассмотрим следующий пример:
main()
{
int a;
printf("%x %x\n",a / 32, a / 10);
}
Листинг 210 Идентификация оператора "/"
Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:
main proc near ; CODE XREF: start+AFp
var_a = dword ptr -4
push ebp
mov ebp, esp
; Открываем кадр стека
push ecx
; Резервируем память для локальной переменной
mov eax, [ebp+var_a]
; Копируем в EAX значение переменной var_a
cdq
; Расширяем EAX до четверного слова EDX:EAX
mov ecx, 0Ah
; Заносим в ECX значение 0xA
idiv ecx
; Делим (учитывая знак) EDX:EAX
на 0xA, занося частное в EAX
; EAX = var_a / 0xA
push eax
; Передаем результат вычислений функции printf
mov eax, [ebp+var_a]
; Загружаем в EAX значение var_a
cdq
; Расширяем EAX до четверного слова EDX:EAX
and edx, 1Fh
; Выделяем пять младших бит EDX
add eax, edx
; Складываем знак числа для выполнения округления отрицательных значений
; в меньшую сторону
sar eax, 5
; Арифметический сдвиг вправо на 5 позиций
; эквивалентен делению числа на 25 = 32
; Таким образом, последние четыре инструкции расшифровываются как:
; EAX = var_a / 32
; Обратите внимание: даже при выключенном режиме оптимизации компилятор
; оптимизировал деление
push eax
push offset aXX ; "%x %x\n"
call _printf
add esp, 0Ch
; printf("%x %x\n", var_a / 0xA, var_a / 32)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 211
А теперь, засучив рукава и глотнув пустырника (или валерьянки) рассмотрим оптимизированный вариант того же примера:
main proc near ; CODE XREF: start+AFp
push ecx
; Резервируем память для локальной переменной var_a
mov ecx, [esp+var_a]
; Загружаем в ECX значение переменной var_a
mov eax, 66666667h
; Так, что это за зверское число?!
; В исходном коде ничего подобного и близко не было!
imul ecx
; Умножаем это зверское число на переменную var_a
; Обратите внимание: именно умножаем, а не делим.
; Однако притворимся на время, что у нас нет исходного кода примера, потому
; ничего странного в операции умножения мы не видим
sar edx, 2
; Выполняем арифметический сдвиг всех битов EDX
на две позиции вправо, что
; в первом приближении эквивалентно его делению на 4
; Однако ведь в EDX находятся старшее двойное слово результата умножения!
; Поэтому, три предыдущих команды фактически расшифровываются так:
; EDX = (66666667h * var_a) >> (32 + 2) = (66666667h * var_a) / 0x400000000
;
; Понюхайте эту строчку – не пахнет ли паленым? Как так не пахнет?! Смотрите:
; (66666667h * var_a) / 0x400000000 = var_a * 66666667h / 0x400000000 =
; = var_a * 0,10000000003492459654808044433594
; Заменяя по всем правилам математики умножение на деление и одновременно
; выполняя округление до меньшего целого получаем:
; var_a * 0,1000000000 = var_a * (1/0,1000000000) = var_a/10
;
; Согласитесь, от такого преобразования код стал намного понятнее!
; Как можно распознать такую ситуацию в чужой программе, исходный текст которой
; неизвестен? Да очень просто – если встречается умножение, а следом за ним
; сдвиг вправо, обозначающий деление, то каждый нормальный математик сочтет
; своим долгом такую конструкцию сократить, по методике показанной выше!
mov eax, edx
; Копируем полученное частное в EAX
shr eax, 1Fh
; Сдвигаем на 31 позицию вправо
add edx, eax
; Складываем: EDX = EDX + (EDX >> 31)
; Чтобы это значило? Нетрудно понять, что после сдвига EDX
на 31 бит вправо
; в нем останется лишь знаковый бит числа
; Тогда – если число отрицательно, мы добавляем к результату деления один,
; округляя его в меньшую сторону. Таким образом, весь этот хитрый код
; обозначает ни что иное как тривиальную операцию знакового деления:
; EDX = var_a / 10
; Не слишком ли много кода для одного лишь деления? Конечно, программа
; здорово "распухает", зато весь этот код выполняется всего лишь за 9 тактов,
; в то время как в не оптимизированном варианте аж за 28!
; /* Измерения проводились на процессоре CLERION
с ядром P6, на других
; процессорах количество тактов может отличается */
; Т.е. оптимизация дала более чем трехкратный выигрыш, браво Microsoft!
mov eax, ecx
; Вспомним: что находится в ECX? Ох, уж эта наша дырявая память, более дырявая
; чем дуршлаг без дна… Прокручиваем экран дизассемблера вверх. Ага, в ECX
; последний раз разгружалось значение переменной var_a
push edx
; Передаем функции printf результат деления var_a
на 10
cdq
; Расширяем EAX (var_a) до четверного слова EDX:EAX
and edx, 1Fh
; Выбираем младшие 5 бит регистра EDX, содержащие знак var_a
add eax, edx
; Округляем до меньшего
sar eax, 5
; Арифметический сдвиг на 5 эквивалентен делению var_a на 32
push eax
push offset aXX ; "%x %x\n"
call _printf
add esp, 10h
; printf("%x %x\n", var_a / 10, var_a / 32)
retn
main endp
Листинг 212
Ну, а другие компиляторы, насколько они продвинуты в плане оптимизации? Увы, ни Borland, ни WATCOM не умеют заменять деление более быстрым умножением для чисел отличных от степени двойки. В подтверждении тому рассмотрим результат компиляции того же примера компилятором Borland C++:
_main proc near ; DATA XREF: DATA:00407044o
push ebp
mov ebp, esp
; Открываем кадр стека
push ebx
; Сохраняем EBX
mov eax, ecx
; Копируем в EAX содержимое неинициализированной регистровой переменной ECX
mov ebx, 0Ah
; Заносим в EBX значение 0xA
cdq
; Расширяем EAX до четверного слова EDX:EAX
idiv ebx
; Делим ECX на 0xA (долго делим – тактов 20, а то и больше)
push eax
; Передаем полученное значение функции printf
test ecx, ecx
jns short loc_401092
; Если делимое не отрицательно, то переход на loc_401092
add ecx, 1Fh
; Если делимое положительно, то добавляем к нему 0x1F для округления
loc_401092: ; CODE XREF: _main+11j
sar ecx, 5
; Сдвигом на пять позиций вправо делим число на 32
push ecx
push offset aXX ; "%x %x\n"
call _printf
add esp, 0Ch
; printf("%x %x\n", var_a / 10, var_a / 32)
xor eax, eax
; Возвращаем ноль
pop ebx
pop ebp
; Закрываем кадр стека
retn
_main endp
Листинг 213
Идентификация оператора "%". Специальной инструкции для вычисления остатка в наборе команд микропроцессоров серии 80x86 нет, - вместо этого остаток вместе с частным возвращается инструкциями деления DIV, IDIV
и FDIVx
(см. идентификация оператора "/").
Если делитель представляет собой степень двойки (2N = b), а делимое беззнаковое число, то остаток будет равен N младшим битам делимого числа. Если же делимое – знаковое, необходимо установить все биты, кроме первых N равными знаковому биту для сохранения знака числа. Причем, если N первых битов равно нулю, все биты результата должны быть сброшены независимо от значения знакового бита.
Таким образом, если делимое – беззнаковое число, то выражение a % 2N
транслируется в конструкцию: "AND a, N", в противном случае трансляция становится неоднозначна – компилятор может вставлять явную проверку на равенство нулю с ветвлением, а может использовать хитрые математические алгоритмы, самый популярный из которых выглядит так: DEC x\ OR x, -N\ INC x. Весь фокус в том, что если первые N бит числа x равны нулю, то все биты результата кроме старшего, знакового бита, будут гарантированно равны одному, а OR x, -N
принудительно установит в единицу и старший бит, т.е. получится значение, равное, –1. А INC –1 даст ноль! Напротив, если хотя бы один из N младших битов равен одному, заема из старших битов не происходит и INC x
возвращает значению первоначальный результат.
Продвинутые оптимизирующие компиляторы могут путем сложных преобразований заменять деление на ряд других, более быстродействующих операций. К сожалению, алгоритмов для быстрого вычисления остатка для всех делителей не существует и делитель должен быть кратен , где k и t – некоторые целые числа.
Тогда остаток можно вычислить по следующей формуле:
Да, эта формула очень сложна и идентификация оптимизированного оператора "%" может быть весьма и весьма непростой, особенно учитывая патологическую любовь оптимизаторов к изменению порядка команд.
Рассмотрим следующий пример:
main()
{
int a;
printf("%x %x\n",a % 16, a % 10);
}
Листинг 214 Идентификация оператора "%"
Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:
main proc near ; CODE XREF: start+AFp
var_4 = dword ptr -4
push ebp
mov ebp, esp
; Открываем кадр стека
push ecx
; Резервируем память для локальной переменной
mov eax, [ebp+var_a]
; Заносим в EAX значение переменной var_a
cdq
; Расширяем EAX до четвертного слова EDX:EAX
mov ecx, 0Ah
; Заносим в ECX значение 0xA
idiv ecx
; Делим EDX:EAX
(var_a) на ECX (0xA)
push edx
; Передаем остаток от деления var_a на 0xA функции printf
mov edx, [ebp+var_a]
; Заносим в EDX значение переменной var_a
and edx, 8000000Fh
; "Вырезаем" знаковый бит и четыре младших бита числа
; в четырех младших битах содержится остаток от деления EDX
на 16
jns short loc_401020
; Если число не отрицательно, то прыгаем на loc_401020
dec edx
or edx, 0FFFFFFF0h
inc edx
; Последовательность сия, как говорилось выше характера для быстрого
; расчета отставка знакового числа
; Следовательно, последние шесть инструкций расшифровываются как:
; EDX = var_a % 16
loc_401020: ; CODE XREF: main+19j
push edx
push offset aXX ; "%x %x\n"
call _printf
add esp, 0Ch
; printf("%x %x\n",var_a % 0xA, var_a % 16)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 215
Любопытно, что оптимизация не влияет на алгоритм вычисления остатка.
Увы, ни Microsoft Visual C++, ни остальные известные мне компиляторы не умеют вычислять остаток умножением.
Идентификация оператора "*".
В общем случае оператор "*" транслируется либо в машинную инструкцию "MUL" (беззнаковое целочисленное умножение), либо в "IMUL" (целочисленное умножение со знаком), либо в "FMULx" (вещественное умножение). Если один из множителей кратен степени двойки, то "MUL" ("IMUL") обычно заменяется командой битового сдвига влево "SHL" или инструкцией "LEA", способной умножать содержимое регистров на 2, 4 и 8. Обе последних команды выполняются за один такт, в то время как MUL
требует в зависимости от модели процессора от двух до девяти тактов. К тому же LEA
за тот же такт успевает сложить результат умножение с содержимым регистра общего назначения и/или константой в придачу. Это позволяет умножать на 3, 5 и 9 просто добавляя к умножаемому регистру его значение. Ну, разве это не сказка? Правда, у LEA есть один недочет – она может вызывать остановку AGI, в конечном счете "съедающую" весь выигрыш в быстродействии на нет.
Рассмотрим следующий пример:
main()
{
int a;
printf("%x %x %x\n",a * 16, a * 4 + 5, a * 13);
}
Листинг 216 Идентификация оператора "*"
Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:
main proc near ; CODE XREF: start+AFp
var_a = dword ptr -4
push ebp
mov ebp, esp
; Открываем кадр стека
push ecx
; Резервируем место для локальной переменной var_a
mov eax, [ebp+var_a]
; Загружаем в EAX значение переменной var_a
imul eax, 0Dh
; Умножаем var_a
на 0xD, записывая результат в EAX
push eax
; Передаем функции printf произведение var_a * 0xD
mov ecx, [ebp+var_a]
; Загружаем в ECX значение var_a
lea edx, ds:5[ecx*4]
; Умножаем ECX на 4 и добавляем к полученному результату 5, записывая его в EDX
; И все это выполняется за один такт!
push edx
; Передаем функции printf результат var_a * 4 + 5
mov eax, [ebp+var_a]
; Загружаем в EAX значение переменной var_a
shl eax, 4
; Умножаем var_a на
16
push eax
; Передаем функции printf произведение var_a * 16
push offset aXXX ; "%x %x %x\n"
call _printf
add esp, 10h
; printf("%x %x %x\n", var_a * 16, var_a * 4 + 5, var_a * 0xD)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 217
За вычетом вызова функции printf и загрузки переменной var_a из памяти на все про все требуется лишь три
такта процессора. А что будет, если скомпилировать этот пример с ключиком "/Ox"? А будет вот что:
main proc near ; CODE XREF: start+AFp
push ecx
; Выделяем память для локальной переменной var_a
mov eax, [esp+var_a]
; Загружаем в EAX значение переменной var_a
lea ecx, [eax+eax*2]
; ECX = var_a * 2 + var_a = var_a * 3
lea edx, [eax+ecx*4]
; EDX = (var_a * 3)* 4 + var_a = var_a * 13!
; Вот так компилятор ухитрился умножить var_a на 13,
; причем всего за один (!) такт. Да, обе инструкции LEA
прекрасно спариваются
; на Pentium MMX и Pentium Pro!
lea ecx, ds:5[eax*4]
; ECX = EAX*4 + 5
push edx
push ecx
; Передаем
функции printf var_a * 13 и var_a * 4 +5
shl eax, 4
; Умножаем var_a на
16
push eax
push offset aXXX ; "%x %x %x\n"
call _printf
add esp, 14h
; printf("%x %x %x\n", var_a * 16, var_a * 4 + 5, var_a * 13)
retn
main endp
Листинг 218
Этот код, правда, все же не быстрее предыдущего, не оптимизированного, и укладывается в те же три такта, но в других случаях выигрыш может оказаться вполне ощутимым.
Другие компиляторы так же используют LEA для быстрого умножения чисел. Вот, к примеру, Borland поступает так:
_main proc near ; DATA XREF: DATA:00407044o
lea edx, [eax+eax*2]
; EDX = var_a*3
mov ecx, eax
; Загружаем в ECX неинициализированную регистровую переменную var_a
shl ecx, 2
; ECX = var_a * 4
push ebp
; Сохраняем EBP
add ecx, 5
; Добавляем к var_a
* 4 значение 5
; Borland
не использует LEA
для сложения. А жаль…
lea edx, [eax+edx*4]
; EDX = var_a + (var_a *3) *4 = var_a * 13
; А вот в этом Borland и MS единодушны :-)
mov ebp, esp
; Открываем кадр стека
; Да, да… вот так посреди функции и открываем…
; Выше, кстати, "потерянная" команда push EBP
push edx
; Передаем printf произведение var_a
* 13
shl eax, 4
; Умножаем ((var_a
*4) + 5) на 16
; Что такое?! Да, это глюк компилятора, посчитавшего: раз переменная var_a
; неинициализирована, то ее можно и не загружать…
push ecx
push eax
push offset aXXX ; "%x %x %x\n"
call printf
add esp, 10h
xor eax, eax
pop ebp
retn
_main endp
Листинг 219
Хотя "визуально" Borland генерирует более "тупой" код, его выполнение укладывается в те же три такта процессора. Другое дело WATCOM, показывающий удручающе отсталый результат на фоне двух предыдущих компиляторов:
main proc near
push ebx
; Сохраняем EBX в стеке
mov eax, ebx
; Загружаем в EAX значение неинициализированной регистровой переменной var_a
shl eax, 2
; EAX = var_a * 4
sub eax, ebx
; EAX = var_a * 4 – var_a = var_a * 3
; Вот каков WATCOM! Сначала умножает "с запасом", а потом лишнее отнимает!
shl eax, 2
; EAX = var_a * 3 * 4 = var_a * 12
add eax, ebx
; EAX = var_a * 12 + var_a = var_a * 13
; Вот так, да? Четыре инструкции, в то время как "ненавистный" многим
; Microsoft Visual C++ вполне обходится и двумя!
push eax
; Передаем printf значение var_a
* 13
mov eax, ebx
; Загружаем в EAX значение неинициализированной регистровой переменной var_a
shl eax, 2
; EAX = var_a * 4
add eax, 5
; EAX = var_a * 4 + 5
; Ага! Пользоваться LEA WATCOM то же не умеет!
push eax
; Передаем printf значение var_a * 4 + 5
shl ebx, 4
; EBX = var_a * 16
push ebx
; Передаем printf значение var_a * 16
push offset aXXX ; "%x %x %x\n"
call printf_
add esp, 10h
; printf("%x %x %x\n",var_a * 16, var_a * 4 + 5, var_a*13)
pop ebx
retn
main_ endp
Листинг 220
В результате, код, сгенерированный компилятором WATCOM требует шести тактов, т.е. вдвое больше, чем у конкурентов.
::Комплексные операторы. Язык Си\Си++ выгодно отличается от большинства своих конкурентов поддержкой комплексных операторов: x= (где x – любой элементарный оператор), ++ и – –.
Комплексные операторы семейства "a x= b" транслируются в "a = a x b" и они идентифицируются так же, как и элементарные операторы (см. "элементарные операторы").
Операторы "++" и "––": в префиксной форме они выражаются в тривиальные конструкции "a = a +1" и "a = a – 1" не представляющие для нас никакого интереса, но вот постфиксная форма – дело другое.
__обращение к разным частям одной переменной