Более эффективный код сборки?
Я недавно начал изучать сборку. Просто интересно, почему эта сборка написана так, как она есть вместо альтернативной "Моя сборка", которую я перечислю ниже. Это вырезает одну инструкцию. Есть идеи? Это слишком редкий случай, когда это работает? Мне кажется, расточительно сначала перенести значение 3 на eax.
Код C:
#include<stdio.h>
int main()
{
int a = 1;
int b = 3;
a = a+b;
return a;
}
Монтаж:
Dump of assembler code for function main:
0x080483dc <+0>: push ebp
0x080483dd <+1>: mov ebp,esp
0x080483df <+3>: sub esp,0x10
0x080483e2 <+6>: mov DWORD PTR [ebp-0x4],0x1
0x080483e9 <+13>: mov DWORD PTR [ebp-0x8],0x3
0x080483f0 <+20>: mov eax,DWORD PTR [ebp-0x8]
0x080483f3 <+23>: add DWORD PTR [ebp-0x4],eax
0x080483f6 <+26>: mov eax,DWORD PTR [ebp-0x4]
0x080483f9 <+29>: leave
0x080483fa <+30>: ret
"Мое собрание":
Dump of assembler code for function main:
0x080483dc <+0>: push ebp
0x080483dd <+1>: mov ebp,esp
0x080483df <+3>: sub esp,0x10
0x080483e2 <+6>: mov DWORD PTR [ebp-0x4],0x1
0x080483e9 <+13>: mov DWORD PTR [ebp-0x8],0x3
0x080483f0 <+20>: add DWORD PTR [ebp-0x4],DWORD PTR [ebp-0x8]
0x080483f3 <+23>: mov eax,DWORD PTR [ebp-0x4]
0x080483f6 <+26>: leave
0x080483f9 <+29>: ret
1 ответ
Как уже сказал Майкл Петч в комментарии, реальный ответ заключается в том, что вы смотрите на неоптимизированный код. Компиляторы делают все... ну, неэффективные вещи в неоптимизированном коде. Иногда они делают это для скорости компиляции. Оптимизация занимает больше времени, чем слепой перевод кода C в инструкции по сборке, поэтому, когда вам нужна грубая скорость, вы выключаете оптимизатор и используете только компилятор: относительно простой переводчик инструкций. Еще одна причина, по которой компиляторы делают неэффективные вещи в неоптимизированном коде, заключается в упрощении отладки. Например, ваша IDE, вероятно, позволяет вам установить точку останова на каждой отдельной строке вашего кода C/C++. Если бы оптимизатор превратил несколько строк C/C++ в одну инструкцию по сборке, вам было бы гораздо труднее, если не невозможно, установить точки останова, которые вы хотите установить. Вот почему отладка оптимизированного кода намного сложнее и часто требует перехода к исходной сборке и выполнения отладки на уровне адресов.
Здесь два мертвых подарка, которые говорят вам, что это неоптимизированный код:
Использование
leave
инструкция, которая по сути является историческим пережитком дней CISC в x86. Философия имела обыкновение иметь кучу инструкций, которые делали сложные вещи, поэтомуenter
Инструкция использовалась в начале функции для установки стекового фрейма, аleave
Инструкция подвела сзади, сносит стек стеллажа. Это облегчило работу программиста в языках с блочной структурой, потому что вам нужно было написать только одну инструкцию для выполнения нескольких задач. Проблема в том, что, по крайней мере, 386, возможно, 286,enter
инструкция была значительно медленнее, чем делать то же самое с более простыми, отдельными инструкциями.leave
также медленнее на 386 и более поздних, и полезно только тогда, когда вы оптимизируете размер по скорости (так как он меньше и не такой медленный, какenter
).Дело в том, что стековый фрейм вообще настраивается! На любом уровне оптимизации 32-разрядный x86-компилятор не потрудится сгенерировать пролог-код, который устанавливает кадр стека. То есть он не сохранит исходное значение
EBP
зарегистрироваться, и это не установитEBP
зарегистрироваться в месте расположения указателя стека (ESP
) при входе в функцию. Вместо этого он выполнит оптимизацию "пропуска указателя кадра" (EBP
регистр называется "указатель кадра"), и вместо использованияEBP
-относительные смещения для доступа к стеку, он будет просто использоватьESP
Относительные смещения. Раньше это было невозможно в 16-битном коде x86, но в 32-битном коде это прекрасно работает, просто требуется больше бухгалтерии, поскольку указатель стека может быть изменен, но указатель кадра может оставаться постоянным. Такая бухгалтерия не является проблемой для компьютера / компилятора, как для человека, так что это очевидная оптимизация.
Еще одна проблема с "вашей" сборкой заключается в том, что вы использовали неверную инструкцию. В архитектуре x86 нет инструкции *, которая принимает два операнда памяти. Самое большее, один из операндов может быть ячейкой памяти. Другой операнд должен быть регистром или непосредственным.
Первая "оптимизированная" версия этого кода будет выглядеть примерно так:
; Allocate 8 bytes of space on the stack for our local variables, 'a' and 'b'.
sub esp, 8
; Load the values of 'a' and 'b', storing them into the allocated locations.
; (Note the use of ESP-relative offsets, rather than EBP-relative offsets.)
mov DWORD PTR [esp], 1
mov DWORD PTR [esp + 4], 3
; Load the value of 'a' into a register (EAX), and add 'b' to it.
; (Necessary because we can't do an ADD with two memory operands.)
mov eax, DWORD PTR [esp]
add eax, DWORD PTR [esp + 4]
; The result is now in EAX, which is exactly where we want it to be.
; (All x86 calling conventions return integer-sized values in EAX.)
; Clean up the stack, and return.
add esp, 8
ret
Мы "оптимизировали" последовательность инициализации стека и потеряли много пуха. Теперь все выглядит довольно хорошо. Фактически, это по сути код, который сгенерирует компилятор, если вы объявите a
а также b
переменные volatile
, Тем не менее, они на самом деле не volatile
в исходном коде, что означает, что мы можем хранить их целиком в регистрах. Это освобождает нас от необходимости делать какие-либо дорогостоящие операции хранения / загрузки памяти и означает, что нам вообще не нужно выделять или восстанавливать пространство стека!
; Load the 'a' and 'b' values into the EAX and EDX registers, respectively.
mov eax, 1
mov edx, 3
; Add 'b' to 'a' in a single operation, since ADD works fine with
; two register operands.
add eax, edx
; Return, with result in EAX.
ret
Аккуратно, верно? Это не только упрощает код, но и дает большой выигрыш в производительности, поскольку мы храним все в регистрах и никогда не должны касаться медленной памяти. Ну, что еще мы можем сделать? Помните, что ADD
Инструкция позволяет нам использовать регистр в качестве операнда назначения и непосредственный в качестве операнда источника. Это означает, что мы могли бы пропустить MOV
и просто сделай:
mov eax, 1
add eax, 3
ret
Это похоже на то, что вы ожидаете увидеть, если, скажем, добавите константу 3 к значению, уже находящемуся в памяти:
add DWORD PTR [esp + 4], 3
Но в этом случае оптимизирующий компилятор никогда бы так не поступил. Это на самом деле перехитрит вас, понимая, что вы добавляете константы времени компиляции, и продолжайте делать это во время компиляции. Таким образом, фактический вывод компилятора - и, действительно, самый эффективный способ написания этого кода - был бы просто:
mov eax, 4
ret
Как анти-климатические.:-) Самый быстрый код - это всегда код, который не должен выполняться.
* По крайней мере, я не могу придумать в данный момент. ISA x86 колоссален, поэтому почти неизбежно, что есть какой-то темный угол, который я не могу вспомнить, где это утверждение ложно. Но это правда, что вы можете положиться на это как на аксиому.