Помимо исправления Алексея, вы можете сделать это значительно более эффективным. (Конечно, встраивание свопа и оптимизация на сайте вызовов еще лучше.)
Нет необходимости в локальном временном хранилище в стеке: вместо этого можно дважды перезагрузить один из адресов или сохранить / восстановить ESI и использовать его в качестве временного.
Вы фактически уничтожаете EBX, который сохраняется во всех обычных соглашениях C о вызовах. В большинстве 32-битных соглашений о вызовах x86 EAX, ECX и EDX - это три регистра с закрытыми вызовами, которые вы можете использовать без сохранения / восстановления, в то время как остальные сохраняются при вызове. (То есть ваш вызывающий ожидает, что вы не уничтожите их значения, поэтому вы можете использовать их, только если вернете исходное значение. Вот почему EBP должен быть восстановлен после использования его для указателя кадра.)
То, что gcc -O3 -m32
делает при компиляции автономного (не встроенного) определения для функции подкачки, это сохранение / восстановление EBX, поэтому он имеет 4 регистра для работы. Clang выбирает ESI.
void swap(int *px, int *py) {
int tmp = *px;
*px = *py;
*py = tmp;
}
В проводнике компилятора Godbolt :
# gcc8.2 -O3 -m32 -fverbose-asm
# gcc itself emitted the comments on the following instructions
swap:
push ebx #
mov edx, DWORD PTR [esp+8] # px, px
mov eax, DWORD PTR [esp+12] # py, py
mov ecx, DWORD PTR [edx] # tmp, *px_3(D)
mov ebx, DWORD PTR [eax] # tmp91, *py_5(D)
mov DWORD PTR [edx], ebx # *px_3(D), tmp91
mov DWORD PTR [eax], ecx # *py_5(D), tmp
pop ebx #
ret
# DWORD PTR is the gas .intel_syntax equivalent of NASM's DWORD
# you can just remove them all because the register implies an operand size
Также избегает создания устаревшего стекового фрейма. Вы можете добавить -fno-omit-frame-pointer
к опциям компилятора, чтобы увидеть code-gen с указателем кадра, если хотите. (Godbolt перекомпилирует и покажет вам asm. Очень удобный сайт для изучения параметров компилятора и изменений кода.)
64-битные соглашения о вызовах уже содержат аргументы в регистрах и имеют достаточно чистых регистров, поэтому мы просто получаем 4 инструкции, гораздо более эффективные.
Как я уже говорил, другой вариант - перезагрузить один из аргументов указателя дважды:
swap:
# without a push, offsets relative to ESP are smaller by 4
mov edx, [esp+4] # edx = px reused later
mov eax, [esp+8] # eax = py also reused later
mov ecx, [edx] # ecx = tmp = *px lives for the whole function
mov eax, [eax] # eax = *py destroying our register copy of py
mov [edx], eax # *px = *py; done with px, can now destroy it
mov edx, [esp+8] # edx = py
mov [edx], ecx # *py = tmp;
ret
Только 7 инструкций вместо 8. Загрузка одного и того же значения дважды - это очень дешево, и выполнение вне очереди означает, что не проблема быстро подготовить адрес магазина, хотя в программном порядке это только инструкция прямо перед магазином, который загружает адрес.