Идентификация математических операторов

Дата публикации 16 июн 2002

Идентификация математических операторов — Архив WASM.RU

"…если вы обессилены, то не удивительно,
что вся ваша жизнь - не развлечение. У вас…
так много вычислений, расчетов, которые
необходимо сделать в вашей жизни, что она
просто не может быть развлечением."

Ошо "Пустая Лодка"
Беседы по высказываниям Чжуан Цзы

  Идентификация оператора "+". В общем случае оператор "+" транслируется либо в машинную инструкцию 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+AF p 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+AF p
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+AF p 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+AF p
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, в то время как правильный ответ - ноль. Вообще же, деление знаковых чисел арифметическим сдвигом вправо дает округление в большую сторону, что совсем не входит в наши планы. Для округления знаковых чисел в меньшую сторону необходимо перед выполнением сдвига добавить к делимому число 2^N- 1, где N - количество битов, на которые сдвигается число при делении. Легко видеть, что это приводит к увеличению всех сдвигаемых битов на единицу и переносу в старший разряд, если хотя бы один из них не равен нулю.

  Следует отметить: деление очень медленная операция, гораздо более медленная чем умножение (выполнение DIV может занять свыше 40 тактов, в то время как MUL обычно укладываться в 4), поэтому, продвинутые оптимизирующие компиляторы заменяют деление умножением. Существует множество формул подобных преобразований, вот, например, она (самая популярная из них): a/b = 2^N/b * a/2^N', где N' - разрядность числа. Выходит, грань между умножением и делением очень тока, а их идентификация довольно сложна. Рассмотрим следующий пример:

main() {
int a; printf("%x %x\n",a / 32, a / 10);
}
  Листинг 210 Идентификация оператора "/"

  Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:

main proc near ; CODE XREF: start+AF p 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 позиций ; эквивалентен делению числа на 2^5 = 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+AF p
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:00407044 o
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+11 j
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 (см. идентификация оператора "/").

  Если делитель представляет собой степень двойки (2^N = b), а делимое беззнаковое число, то остаток будет равен N младшим битам делимого числа. Если же делимое - знаковое, необходимо установить все биты, кроме первых N равными знаковому биту для сохранения знака числа. Причем, если N первых битов равно нулю, все биты результата должны быть сброшены независимо от значения знакового бита.

  Таким образом, если делимое - беззнаковое число, то выражение a % 2^N транслируется в конструкцию: "AND a, N", в противном случае трансляция становится неоднозначна - компилятор может вставлять явную проверку на равенство нулю с ветвлением, а может использовать хитрые математические алгоритмы, самый популярный из которых выглядит так: DEC x\ OR x, -N\ INC x. Весь фокус в том, что если первые N бит числа x равны нулю, то все биты результата кроме старшего, знакового бита, будут гарантированно равны одному, а OR x, -N принудительно установит в единицу и старший бит, т.е. получится значение, равное, -1. А INC -1 даст ноль! Напротив, если хотя бы один из N младших битов равен одному, заема из старших битов не происходит и INC x возвращает значению первоначальный результат.

  Продвинутые оптимизирующие компиляторы могут путем сложных преобразований заменять деление на ряд других, более быстродействующих операций. К сожалению, алгоритмов для быстрого вычисления остатка для всех делителей не существует и делитель должен быть кратен k * 2^t, где k и t - некоторые целые числа. Тогда остаток можно вычислить по следующей формуле: a % b = a % k*2^t = a -((2^N/k * a/2^N) & -2^t)*k

  Да, эта формула очень сложна и идентификация оптимизированного оператора "%" может быть весьма и весьма непростой, особенно учитывая патологическую любовь оптимизаторов к изменению порядка команд.

  Рассмотрим следующий пример:

main() {
int a; printf("%x %x\n",a % 16, a % 10);
}
  Листинг 214 Идентификация оператора "%"

  Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:

main proc near ; CODE XREF: start+AF p 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+19 j
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+AF p 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+AF p
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:00407044 o
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 единодушны :smile3: 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" не представляющие для нас никакого интереса, но вот постфиксная форма - дело другое.

  Продолжение следует... © Крис Касперски


0 2.348
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532