Абсолютное значение без разветвлений и сдвигов, только add / sub и booleans - PullRequest
2 голосов
/ 13 апреля 2020

Мы получили эту проблему в школе для студентов, которые хотят проверить себя. Я потратил довольно много времени на это, но не могу понять это.

У вас есть 16-битный номер в регистре AX, этот номер подписан. Получите его абсолютное значение, число в AX должно быть неизменным (РЕДАКТИРОВАТЬ: Число регистров не ограничено, и регистр AX можно изменить, но в конце функции это должен быть исходный номер), и Ответ должен быть в BX. Вы можете использовать только эти инструкции:
MOV, ADD, XOR, SUB, NOT, AND, OR, NEG.

Это довольно легко сделать с SAR, как это делают компиляторы, но без неясно, как получить какое-либо поведение, обусловленное битом знака.

Ответы [ 2 ]

3 голосов
/ 13 апреля 2020

Глупая идея № 1: Таблица поиска. Это не может работать в 16-битном реальном режиме. Даже целого сегмента размером 64 КБ для таблицы недостаточно; нам нужно вдвое больше, чтобы иметь возможность искать 2-байтовый результат для любого возможного 16-битного значения.

Мы могли бы легко сделать это с 32-битной адресацией, например xor ebx, ebx / mov bx, ax / mov bx, [table + ebx*2], если вы можете оправдать 128 кБ данных таблицы. : P

Полностью в рамках правил вы можете построить таблицу в стеке в 32-битном или 64-битном режиме с помощью sub esp, 1<<17 и сохранить данные с помощью mov word [esp+0], 0 / mov word [esp + 2], 1 / et c. Полностью развернут, без зацикливания, так что около 256 кБ машинного кода. Но, опять же, это не работает в реальном режиме и является полной шуткой для эффективности.


Мы можем использовать shenanigans с частичным регистром x86, чтобы изолировать знаковый бит как целое число 0/1:

    xor  dx, dx           ; DX = 0
    mov  dl, ah           ; DX = AX>>8   (zero extended)
    add  dx, dx           ; DX <<= 1  shifts the sign bit alone into DH

    mov  dl, dh
    mov  dh, 0            ; DX = (AX<0) = sign bit of AX zero extended to 16-bit

    neg  dx               ; DX = 0 or -1

Или последние 3 инструкции могут быть оптимизированы до 2

    neg  dh               ; 0 or -1 according to sign bit of AX
    mov  dl, dh           ; duplicate to the full DX = 0 or -1

Джекпот; у нас есть значение sar ax,15 или cwd, в котором есть все биты 0 или все биты 1, передающие знаковый бит AX, готовые к использованию с идентификатором дополнения 2 ( Как доказать, что оператор C - x, ~ x + 1 и ~ (x-1) дают одинаковые результаты? ), как используют компиляторы (https://godbolt.org/z/n3yoUp).

Обычно вы используете xor ax, dx / sub ax, dx для изменения исходного значения.

Раньше я думал, что задача потребовала от вас избегать изменения любых других регистров, в противном случае ограничение на оставление AX без изменений является тривиальным и не стоит сделать часть проблемы. Но я не думаю, что это возможно без дополнительного места в памяти или другого регистра. Редактирование разъяснило, что в этом нет необходимости.

    mov  bx, ax
    xor  bx, dx           ; ~x      or x
    sub  bx, dx           ; ~x + 1  or x

XOR с -1 переворачивает все биты, как НЕ. XOR с 0 не используется.

SUB с шагом -1 на 1, SUB с 0 не используется. (0 является элементом идентификации для сложения и xor.)

Таким образом, это условно применяет идентичность дополнения 2 -x = ~x + 1.


PS: Мне потребовалось несколько минут головы Я думаю об этом, исключаю любые подходы с полным регистром, и я очень знаком с x86 и довольно хорошо разбираюсь с битовыми манипуляциями, например, пишу ответы codegolf.SE в машинном коде x86 и делаю не тривиальные вещи с SIMD. IMO, это сложный и сложный вызов.

Кроме того, вы никогда не захотите писать такой код в реальной жизни; cwd или cdq гораздо эффективнее, или для исходных регистров, отличных от AX, copy и sar. Частичная регистрация даже приведет к остановке некоторых неиспользуемых исполнительных процессоров, таких как Intel PPro и Nehalem.


Например, на проводнике компилятора Godbolt для этого источника :

unsigned absval(int x) {
    return x<0 ? 0U - x : x;
}

Использование возвращаемого значения без знака позволяет нам избежать неопределенного поведения переполнения со знаком со знаком для наиболее отрицательного целого числа дополнения 2. (-INT_MIN - неопределенное поведение). Я думаю, что способ, которым я написал, на самом деле основывается на реализации C, являющейся дополнением к 2, хотя, потому что 0U - x преобразует x в unsigned для соответствия другой стороне перед , используя его как операнд для двоичного кода -. Или, может быть, это то, что мы хотим, чтобы беззнаковый 0U-x производил 0x8000 из входа 0x8000 (для 16-битного целого).

G CC делает это для установки EAX = abs ( EDI) (x86-64 System V соглашение о вызовах).

    mov     eax, edi
    cdq                      ; sign-extend EAX into EDX:EAX
    xor     eax, edx
    sub     eax, edx
    ret

clang делает это для x86-64, используя условное перемещение, которое считывает флаги из NEG:

    mov     eax, edi
    neg     eax                 ; 0 - x
    cmovl   eax, edi            ; copy the original if 0 was < x
    ret

это будет на некоторых процессорах более эффективны:

    ; shorter critical path on CPUs where mov is not zero latency
    xor     eax, eax
    sub     eax, edi            ; 0 - x
    cmovl   eax, edi            ; copy the original if 0 was < x
    ret

Sandybridge устраняет обнуление по оси X, но не перемещает, а для процессоров, которые не устраняют mov, это сокращает критический путь. mov eax,edi находится на критическом пути, но xor - ноль. Или мы могли бы сделать mov eax, edi / neg edi / cmovnl eax, edi, чтобы снова разрешить параллельную работу MOV и NEG.

CMOV - это команда из двух операций на процессорах Intel до Broadwell. (CMOVA и CMOVBE по-прежнему являются 2 мопами на текущем Intel, потому что они читают CF и ZF, которые переименованы отдельно в разных группах. Другие 1 моп)

0 голосов
/ 13 апреля 2020

Таким образом, благодаря ответу Питера Кордеса, код довольно прост, проблема заключалась в инструкции SAR, но Питер создал ее очень хорошо.

Номер уже загружен в AX

; this is practicaly the SAR instruction, 
; the mask for the absolute value is 
; number >> (sizeof(short)) * CHAR_BIT -1)
; number >>        (2 * 8) - 1 = 15
; so normaly we would do SAR bx, 15 and done

mov bl, ah  ; BX = AX>>8
add bx, bx  ; BX <<= 1
neg bh      ; 0 or -1 
mov bl, bh  ; duplicate the full BX

mov cx, ax  ;
add cx, bx  ; the number + mask 
xor bx, cx  ; (number+mask) ^ mask 

теперь ответ в BX, а AX не был изменен

...