Умножение со знаком и последующее деление в 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

Но я не уверен в следующих вещах.

  1. Действительно ли необходимо делать те шаги, которые я сделал? Это решение "наименьшее количество строк"?
  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.)

Другие вопросы по тегам