Умножение со знаком и последующее деление в YASM (архитектура x86_64)
Я использую yasm
ассемблер для x86_64
Архитектура процессора. Предположим, у меня уже есть три числа, определенные в .data
раздел:
section .data
;CONSTANTS:
SYSTEM_EXIT equ 60
SUCCESS_EXIT equ 0
;VARIABLES:
dVar1 dd 40400
wVar2 dw -234
bVar3 db -23
dRes dd 0 ;quotient
dRem dd 0 ;reminder
И что я хочу сделать, это умножить двойное слово со знаком dVar1
подписанным словом dVar2
с последующим делением на подписанный байт bVar3
,
Ниже я представлю свое "решение" со ссылкой на эту книгу, чтобы узнать, почему я делаю каждый шаг. Вопросы в конце текста.
dVar1 * wVar2 (подписано)
Я не вижу явного правила, согласно которому умножение применяется только к числам одинакового размера. Но посмотрим некоторые неявные. Вот почему я использую преобразование для wVar2
:
movsx eax, word [wVar2] ;[wVar2] now in eax
Теперь "они" имеют одинаковый размер, поэтому я просто умножаю их:
imul dword [dVar1] ;edx:eax = eax * [dVar1]
... Например, результат умножения на топор (16 бит) на слово операнд (также 16 бит) дает результат двойного слова (32 бита). Тем не менее, результат не помещается в EAX (что может быть проще), он помещается в два регистра, DX для результата верхнего порядка (16-бит) и топор для результата нижнего порядка (16 бит), типично записывается как dx: ax (по договоренности).
Как я правильно понимаю, результат сейчас находится в edx:eax
,
edx:eax / bVar3 (подписано)
... для дивидендов требуется регистр D (для части верхнего порядка) и A (для части нижнего порядка)... Если было выполнено предыдущее умножение, регистры D и A уже могут быть установлены правильно (что является моим кейс [примечание ОП]).
а также
... Кроме того, A и, возможно, регистр D должны использоваться в комбинации для получения дивиденда.
- Деление байта: топор для 16 бит
- Слово делится: dx: топор для 32-битных
- Разделение по двойному слову: edx: eax для 64-битного кода (в моем случае [примечание OP])
- Разделение четырех слов: rdx: rax для 128-бит
Так что изначально я конвертирую bVar3
двойное слово, а затем просто разделить его:
movsx ebx, byte [bVar3] ;ebx = [bVar3]
idiv ebx, ;eax = edx:eax / [bVar3]
Весь код тогда
section .data
;CONSTANTS:
SYSTEM_EXIT equ 60
SUCCESS_EXIT equ 0
;VARIABLES:
dVar1 dd 40400
wVar2 dw -234
bVar3 db -23
dRes dd 0 ;quotient
dRem dd 0 ;reminder
section .text
global _start
_start:
movsx ebx, byte [bVar3] ;conversion to double-word
movsx eax, word [wVar2] ;conversion to double-word
imul dword [dVar1] ;edx:eax = eax * [dVar1]
idiv ebx ;eax = edx:eax / [bVar3], edx = edx:eax % [bVar3]
mov dword [dRes], eax
mov dword [dRem], edx
last:
mov rax, SYSTEM_EXIT
mov rdi, SUCCESS_EXIT
syscall
Я использую отладчик и вижу правильный ответ:
(gdb) x/dw &dRes
0x600159: 411026
(gdb) x/dw &dRem
0x60015d: -2
Но я не уверен в следующих вещах.
- Действительно ли необходимо делать те шаги, которые я сделал? Это решение "наименьшее количество строк"?
- Это правильное решение вообще? Я имею в виду, что мог ошибиться или пропустить что-то важное здесь.
PS Может быть, этот вопрос скорее вопрос CodeReview SE. Дайте мне знать, если вы тоже так думаете.
1 ответ
Это решение "наименьшее количество строк"?
Ваш код выглядит хорошо, и в нем нет напрасных инструкций или очевидной эффективности (за исключением вашего системного вызова, где mov
до 64-битных регистров - пустая трата размера кода).
Но сделать 2-й movsx
после обеих других нагрузок. Внеочередное выполнение не анализирует цепочки зависимостей и сначала загружает критический путь. 2-й movsx
нагрузка не нужна, пока imul
результат готов, так что ставьте его после imul
итак первые 2 нагрузки (movsx
и imul
операнд памяти) может выполняться как можно раньше и позволить imul
Начните.
Оптимизация asm для наименьшего количества инструкций (строк исходного текста), как правило, не полезна / не важна. Либо перейдите к размеру кода (наименьшее количество байтов машинного кода) или к производительности (наименьшее количество мопов, минимальная задержка и т. Д., См . Руководство по оптимизации Agner Fog и другие ссылки в вики-теге x86). Например, idiv
микрокодируется на процессорах Intel, и на всех процессорах работает намного медленнее, чем любая другая инструкция, которую вы использовали.
В архитектурах с инструкциями фиксированной ширины количество инструкций является прокси для размера кода, но это имеет место в x86 с инструкциями переменной длины.
Во всяком случае, нет хорошего способа избежать idiv
тем не менее, если делитель не является константой времени компиляции: почему GCC использует умножение на странное число при реализации целочисленного деления? и 32-битный размер операнда (с 64-битным дивидендом) является самой маленькой / самой быстрой версией, которую вы можете использовать. (В отличие от большинства инструкций, div
быстрее с более узкими операндами).
Для размера кода вы можете использовать один RIP-относительный lea rdi, [rel dVar1]
, а затем получить доступ к другим переменным, таким как [rdi + 4]
, который занимает 2 байта (modr/m + disp8) вместо 5 байтов (modr/m + rel32). т.е. 1 дополнительный байт на каждый операнд памяти (по сравнению с источником регистра).
Было бы целесообразно распределить местоположения результатов ваших dword перед расположением слов и байтов, чтобы все dwords были естественным образом выровнены, и вам не нужно было беспокоиться о снижении производительности из-за их разделения на строку кэша. (Или использовать align 4
после db
перед этикеткой и dd
).
Единственная опасность здесь заключается в том, что 64/32 => 32-битное деление может переполниться и выйти из строя (с #DE
, что приводит к SIGFPE в Linux), если частное (dVar1*wVar2) / bVar3
не помещается в 32-битный регистр Вы можете избежать этого, используя 64-битное умножение и деление, как это делает компилятор, если это вызывает озабоченность. Но учтите, что 64-битный idiv
примерно в 3 раза медленнее, чем 32-битный idiv
на Haswell / Skylake. ( http://agner.org/optimize/)
; fully safe version for full range of all inputs (other than divide by 0)
movsx rcx, byte [bVar3]
movsxd rax, dword [dVar1] ; new mnemonic for x86-64 dword -> qword sign extension
imul rax, rcx ; rax *= rcx; rdx untouched.
cqo ; sign extend rax into rdx:rax
movsx rcx, word [wVar2]
idiv rcx
mov qword [qRes], rax ; quotient could be up to 32+16 bits
mov dword [dRem], edx ; we know the remainder is small, because the divisor was a sign-extended byte
Это, очевидно, больший размер кода (больше инструкций, и у большего количества из них есть префиксы REX для использования 64-битного размера операнда), но менее очевидно, что он намного медленнее, потому что 64-битный idiv
медленный, как я уже говорил ранее.
С помощью movsxd
до 64-битной imul
с двумя явными операндами лучше на большинстве процессоров, но на нескольких процессорах, где 64-битные imul
медленно (семейство AMD Bulldozer или Intel Atom), вы можете использовать
movsx eax, byte [bVar3]
imul dword [dVar1] ; result in edx:eax
shl rdx, 32
or rax, rdx ; result in rax
На современных основных процессорах, тем не менее, 2-операнд imul
быстрее, потому что он должен написать только один регистр.
Кроме выбора инструкции:
Вы помещаете свой код в .data
раздел! использование section .text
до _start:
или поставьте свои данные в конце. (В отличие от C, вы можете ссылаться на символ в источнике раньше, чем он был объявлен, как метки, так и equ
константы. Только ассемблерные макросы (%define foo bar
) применяются по порядку).
Кроме того, ваши исходные данные могут войти в section .rodata
и ваши выводы могут пойти в BSS. (Или оставьте их в регистрах, если ваше назначение не требует памяти; ничто не использует их.)
Используйте RIP-относительную адресацию вместо 32-битного абсолюта: default rel
директива не используется по умолчанию, но RIP-относительный кодируется на 1 байт короче, чем [abs dVar1]
, (32-битный абсолют работает в 64-битных исполняемых файлах, но не в 64-битных позиционно-независимых исполняемых файлах).
Если это нормально для div
к ошибке, если частное не умещается в 32 бита (как ваш существующий код), в этой версии есть все исправления, которые я предложил:
default rel ; RIP-relative addressing is more efficient, but not the default
;; section .text ; already the default section
global _start
_start:
movsx eax, word [wVar2]
imul dword [dVar1] ;edx:eax = eax * [dVar1]
movsx ecx, byte [bVar3]
idiv ecx ;eax = edx:eax / [bVar3], edx = edx:eax % [bVar3]
; leaving the result in registers is as good as memory, IMO
; but presumably your assignment said to store to memory.
mov dword [dRes], eax
mov dword [dRem], edx
.last: ; local label, or don't use a label at all
mov eax, SYS_exit
xor edi, edi ; rdi = SUCCESS_EXIT. don't use mov reg,0
syscall ; sys_exit(0), 64-bit ABI.
section .bss
dRes: resd 1
dRem: resd 1
section .rodata
dVar1: dd 40400
wVar2: dw -234
bVar3: db -23
; doesn't matter what part of the file you put these in.
; use the same names as asm/unistd.h, SYS_xxx
SYS_exit equ 60
SUCCESS_EXIT equ 0
Стиль: использование :
после меток, даже если это не обязательно. dVar1: dd 40400
вместо dVar1 dd 40400
, Это хорошая привычка, если вы случайно используете имя метки, совпадающее с мнемоникой инструкции. подобно enter dd 40400
может дать сбивающее с толку сообщение об ошибке, но enter: dd 40400
будет просто работать.
Не использовать mov
чтобы установить регистр в ноль, используйте xor same,same
потому что он меньше и быстрее. И не mov
в 64-битный регистр, когда вы знаете, что ваша константа мала, пусть неявное нулевое расширение сделает свою работу. (YASM не оптимизирует mov rax,60
в mov eax,60
для вас, хотя NASM делает). Почему инструкции x86-64 для 32-битных регистров обнуляют верхнюю часть полного 64-битного регистра?,
Я не вижу явного правила, согласно которому умножение применяется только к числам одинакового размера. Но посмотрим некоторые неявные.
(Почти) все инструкции x86 требуют одинакового размера для всех своих операндов. Исключения включают movzx
/ movsx
, shr reg, cl
(и другие переменные счетчики сдвигов / вращений), и тому подобное movd xmm0, eax
которые копируют данные между различными наборами регистров. Также операнды управления imm8, такие как pshufd xmm0, xmm1, 0xFF
,
Инструкции, которые обычно работают с входами / выходами одинакового размера, не имеют версий, расширяющих один из входов на лету.
Ты это видишь imul
явно документирует, с какими размерами он работает, в справочном руководстве Intel по набору инструкций для него. Единственная форма, которая делает 32x32 => 64-битный результат IMUL r/m32
т.е. явный операнд должен быть 32-битным регистром или памятью, чтобы идти с неявным eax
исходный операнд.
Так да, movsx
загрузки из памяти, безусловно, лучший способ реализовать это. (Но не единственный; cbw
/ cwde
также работает, чтобы подписать расширение в EAX, и всегда есть shl eax, 16
/ sar eax,16
знак простирается от 16 до 32. Это намного хуже, чем movsx
-load.)