Зачем использовать push / pop вместо sub и mov? - PullRequest
2 голосов
/ 26 марта 2020

Когда я играю с разными компиляторами на https://godbolt.org, я замечаю, что компиляторы очень часто генерируют код, подобный этому:

push    rax
push    rbx
push    rcx
call    rdx
pop     rcx
pop     rbx
pop     rax

Я понимаю, что каждый push или pop делает две вещи:

  1. перемещает операнд в / из стекового пространства
  2. увеличивает / уменьшает указатель стека (rsp)

Таким образом, в нашем примере выше, я предполагаю, что ЦП фактически выполняет 12 операций (6 ходов, 6 добавок / переходов), не включая call. Разве не было бы более эффективно объединить добавления / сабы? Например:

sub rsp, 24
mov [rsp-24], rax
mov [rsp-16], rbx
mov [rsp-8], rcx
call    rdx
mov rcx, [rsp-8]
mov rbx, [rsp-16]
mov rax, [rsp-24]
add rsp, 24

Теперь есть только 8 операций (6 ходов, 2 добавления / саба), не включая call. Почему компиляторы не используют этот подход?

1 Ответ

7 голосов
/ 26 марта 2020

Если вы компилируете с -mtune=pentium3 или с чем-то более ранним, чем -mtune=pentium-m, G CC будет делать код, как вы представляли, потому что на этих старых процессорах push / pop действительно декодирует в отдельная операция ALU для указателя стека, а также загрузка / сохранение. (Вам придется использовать -m32 или -march=nocona (64-битный P4 Prescott), потому что эти старые процессоры также не поддерживают x86-64). Почему g cc использует movl вместо pu sh для передачи аргументов функции?

Но Pentium-M ввел "движок стека" во внешнем интерфейсе, который устраняет стек - регулировка части стека операций, таких как push / call / ret / pop. Он эффективно переименовывает указатель стека с нулевой задержкой. См. Руководство по микроарху Agner Fog и Что такое механизм стека в микроархитектуре Sandybridge?

В качестве общей тенденции любая инструкция, которая широко используется в существующих двоичных файлах, будет мотивировать Разработчики процессора, чтобы сделать это быстро. Например, Pentium 4 пытался заставить всех отказаться от использования INC / DEC; это не сработало; современные процессоры делают переименование с частичным флагом лучше, чем когда-либо . Современные транзисторы x86 и бюджеты мощности могут поддерживать такую ​​сложность, по крайней мере, для процессоров с большим ядром (не Atom / Silvermont). К сожалению, я не думаю, что есть надежда на ложные зависимости (от места назначения) для таких инструкций, как sqrtss или cvtsi2ss, хотя.


Использование указателя стека в инструкции явно например, add rsp, 8 требует, чтобы механизм стека в процессорах Intel вставлял syn c uop для обновления значения регистра, вышедшего из строя. То же самое, если внутреннее смещение становится слишком большим.

Фактически pop dummy_register на больше эффективнее, чем add rsp, 8 или add esp,4 на современных процессорах, поэтому компиляторы, как правило, используют его для получения одного слот стека с настройкой по умолчанию или, например, -march=sandybridge. Почему эта функция выводит sh RAX в стек в качестве первой операции?

См. Также Какой компилятор C / C ++ может использовать команды pu sh pop для создания локальных инструкций переменных вместо простого увеличения esp один раз? re: использование push для инициализации локальных переменных в стеке вместо sub rsp, n / mov. В некоторых случаях это может быть выигрышем, особенно для размера кода с небольшими значениями, но компиляторы этого не делают.


Кроме того, нет, G CC / clang не будет создавать код это точно как то, что вы показываете.

Если им нужно сохранить регистры вокруг вызова функции, они обычно делают это, используя mov в памяти. Или mov в регистр, сохраняющий вызов, который они сохранили в верхней части функции и восстановят в конце.

Я никогда не видел G CC или clang pu sh множественный вызов регистры перед вызовом функции, кроме передачи аргументов стека. И, безусловно, не несколько попсов, чтобы потом восстановить в одни и те же (или разные) регистры. Разлив / перезагрузка внутри функции обычно использует mov. Это исключает возможность push / pop внутри al oop (за исключением передачи аргументов стека в call) и позволяет компилятору выполнять ветвление, не беспокоясь о совпадении push-вызовов с pops. Кроме того, это уменьшает сложность метаданных по разворачиванию стека, которые должны иметь запись для каждой инструкции, которая перемещает RSP. (Интересный компромисс между количеством команд в сравнении с метаданными и размером кода для использования RBP в качестве традиционного указателя фрейма.)

Что-то как ваш код-генератор можно увидеть с помощью регистров с сохранением вызовов + некоторые reg-reg перемещает крошечную функцию, которая просто вызывала другую функцию, а затем возвращает __int128, которая была функцией arg в регистрах. Таким образом, входящий RSI: RDI должен быть сохранен для возврата в RDX: RAX.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...