Язык ассемблера - вычтите edx из ecx и поместите результат в ebx

Как бы я сделал это, не меняя какой-либо другой регистр (он же оставил ecx & edx такими же, как раньше)?

В C ++ это было бы так:

int ecx = 3;
int edx = 1;
int ebx = ecx - edx;

Пока что я сделал это:

mov ecx, 1
mov edx, 3
sub ecx, edx
mov ebx, ecx

person Panda Assassin    schedule 04.07.2018    source источник
comment
Что ты пробовал делать?   -  person UnholySheep    schedule 05.07.2018
comment
Просто вычтите edx из ecx, а затем сделайте ebx равным ecx, но тогда мне придется изменить значение ecx обратно   -  person Panda Assassin    schedule 05.07.2018
comment
Рассматривали ли вы сначала скопировать значение ecx в ebx, прежде чем выполнять sub непосредственно на ebx? (На самом деле это то, что делают и большинство компиляторов C ++)   -  person UnholySheep    schedule 05.07.2018
comment
Нет lea, но вычитайте вместо добавления, если вы это имеете в виду, вам нужны другие уловки   -  person harold    schedule 05.07.2018
comment
Да, но мне просто интересно, есть ли способ сделать это без этого   -  person Panda Assassin    schedule 05.07.2018
comment
mov ebx,ecx перед sub ebx,edx является наиболее оптимальным. Вы можете добиться того же многими другими способами, например, neg edx lea ebx,[ecx+edx] neg edx или используя push/pop, как в ответе, но все эти варианты искусственно сложны, чтобы избежать самого простого и иметь дополнительное снижение производительности.   -  person Ped7g    schedule 05.07.2018


Ответы (2)


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

# with ecx and edx holding your inputs (which I'm calling C and D).

mov  ebx, ecx      ; ebx = C
sub  ebx, edx      ; ebx = C - D

Это лучшее, что вы можете сделать в этом случае, когда вам нужно не уничтожать значения в ECX и EDX.

Если у вас мало доступных регистров, хорошим вариантом может быть сохранение ECX в стеке, а затем получение результата C - D в ECX вместо нового регистра.

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

Компиляторы обычно неплохо справляются с распределением регистров, но их код может быть трудночитаемым, потому что они даже не пытаются согласовываться с использованием регистров. Для неразрушающих операций они часто без причины помещают результат в новый регистр. Тем не менее, вывод компилятора часто является хорошей отправной точкой для оптимизации. (Напишите крошечную функцию, которая что-то делает, и посмотрите, как она компилируется. Или напишите все на C с аргументами функции вместо констант в качестве входных данных и скомпилируйте ее.)


x86 имеет некоторую инструкцию копирования и эксплуатации для других операций (не sub), в первую очередь LEA.

lea  ebx, [ecx + ecx*4]     ; ebx = C * 5
lea  ebx, [ecx + ebx - 2]   ; ebx = C + D - 2

Режимы адресации x86 могут добавлять или вычитать константы, но могут только сдвиг влево и добавление регистров.


форма непосредственного операнда imul также является 3-операндом, для использования с множителями, которые вы можете не годится для 1 или 2 LEA:

imul   ebx,  ecx,  0x01010101     ;  ebx = cl repeated 4 times, if upper bytes were zero

В отличие от большинства инструкций с непосредственным операндом, imul не не перегружает /r в байте ModRM как дополнительные биты кода операции. Таким образом, у него есть место для кодирования места назначения регистра и источника reg / mem, потому что 186 выделил ему целый байт кода операции.


Расширения ISA, такие как BMI1 и BMI2, добавили несколько новых целочисленных инструкций с 3 операндами, например ANDN и SHRX.

andn   ebx,  ecx, edx             ; ebx = (~C) & D   ; BMI1

shrx   ebx,  edx, ecx             ; ebx = D >> C     ; BMI2

Но они доступны не повсеместно, только Haswell, а затем и Ryzen. (И версии Haswell / Skylake для Pentium / Celeron по-прежнему продаются без них, что еще больше откладывает момент, когда они станут базовыми. Спасибо, Intel.)

И, конечно же, для векторных инструкций AVX предоставляет неразрушающие версии всех инструкций SSE.

movaps    xmm2, xmm0         ; copy a whole register
subsd     xmm2, xmm1         ; scalar double-precision FP subtract: xmm0-xmm1

vsubsd    xmm3, xmm0, xmm1

или менее очевидный вариант использования

xorps     xmm0, xmm0    ; zero the register and break any false dependencies
cvtsi2sd  xmm0, eax     ; convert to double-precision FP, with the upper element = 0

xorps     xmm1, xmm1
cvtsi2sd  xmm1, edx

по сравнению с AVX:

vxorps    xmm1,  xmm1,xmm1   ; xmm1 = all-zero

vcvtsi2sd  xmm0, xmm1, eax
vcvtsi2sd  xmm1, xmm1, edx

Это повторно использует тот же обнуленный регистр в качестве места назначения слияния, чтобы избежать ложных зависимостей (и имеет нулевые старшие 64 бита 128-битного регистра).

person Peter Cordes    schedule 04.07.2018
comment
ты всегда можешь ... - Это было небольшое преувеличение. Вы не можете делать x = y - x с помощью MOV + SUB, хотя можете делать neg eax / add eax, edx. По-прежнему 2 инструкции, но ни одна из них не mov, поэтому удаление с помощью команды mov не снижает задержку до 1 цикла. (Как это было бы на машине с 3 операндами, такой как AArch64 с sub w0, w1, w0.) - person Peter Cordes; 21.06.2021

Вы всегда можете использовать стек для сохранения регистров:

    push ecx
    push edx
    mov ecx, 1
    mov edx, 3
    sub ecx, edx
    mov ebx, ecx
    pop edx
    pop ecx
person OregonJim    schedule 04.07.2018
comment
Это дает мне случайное число для ecx и edx - person Panda Assassin; 05.07.2018
comment
@PandaAssassin: В вашем вопросе говорится, что вы хотите, чтобы ECX и EDX оставались такими же, как раньше. ECX в этом коде будет тем, что было в ECX до push ecx. Думаю, ваш вопрос был неправильным? - person Michael Petch; 05.07.2018
comment
@MichaelPetch Теперь я понял. Я просто пытался сохранить ecx как 3. Для меня я бы просто поставил push после того, как я mov ecx и edx - person Panda Assassin; 05.07.2018
comment
Это правильно, но слишком сложно. У меня есть соблазн проголосовать против этого, потому что загромождение вашего кода лодкой push / pop затрудняет чтение. - person Peter Cordes; 05.07.2018
comment
@PeterCordes, два - это не так уж много. Трудно читать? Вы никогда не видели типичного режима прерывания? - person OregonJim; 05.07.2018
comment
Я хотел сказать, что поощрение людей использовать push / pop, когда в этом нет необходимости, приводит к нечитаемому коду для начинающих, который сохраняет все регистры вокруг каждой функции или даже внутри отдельных функций. Я не говорю, что 2 - это слишком много. Кстати, исходный код OP C ++ оставляет EDX и ECX измененными, поэтому, если вам нужно mov reg,imm внутри push / pop, вы можете просто оптимизировать его до mov ebx, 1-3. (И, к сведению, я не был сторонником этого ответа. Кто-то еще проголосовал против сразу после того, как я сказал, что я соблазнялся сделать это. Вы, вероятно, подумали, что это был я, и были очень раздражены.) - person Peter Cordes; 05.07.2018