Добавить против мул (IA32-Assembly)
Я знаю, что добавить быстрее по сравнению с многофункциональным.
Я хочу знать, как использовать add вместо mul в следующем коде, чтобы сделать его более эффективным.
Образец кода:
mov eax, [ebp + 8] #eax = x1
mov ecx, [ebp + 12] #ecx = x2
mov edx, [ebp + 16] #edx = y1
mov ebx, [ebp + 20] #ebx = y2
sub eax,ecx #eax = x1-x2
sub edx,ebx #edx = y1-y2
mul edx #eax = (x1-x2)*(y1-y2)
5 ответов
add быстрее, чем mul, но если вы хотите умножить два общих значения, mul гораздо быстрее, чем любой цикл, выполняющий операции add.
Вы не можете серьезно использовать add, чтобы заставить этот код работать быстрее, чем с mul. Если вам нужно умножить на некоторое небольшое постоянное значение (например, 2), то, возможно, вы могли бы использовать add, чтобы ускорить процесс. Но для общего случая - нет.
Если вы умножаете два значения, которые вы не знаете заранее, фактически невозможно превзойти инструкцию умножения в ассемблере x86.
Если вы заранее знаете значение одного из операндов, вы можете превзойти инструкцию умножения, используя небольшое количество добавлений. Это особенно хорошо работает, когда известный операнд мал и имеет только несколько битов в двоичном представлении. Чтобы умножить неизвестное значение x на известное значение, состоящее из 2^p+2^q+...2^r, вы просто добавляете x*2^p+x*2^q+..x*2*r, если биты p,q, ... и r установлены. Это легко достигается в ассемблере путем сдвига влево и добавления:
; x in EDX
; product to EAX
xor eax,eax
shl edx,r ; x*2^r
add eax,edx
shl edx,q-r ; x*2^q
add eax,edx
shl edx,p-q ; x*2^p
add eax,edx
Основная проблема заключается в том, что для этого требуется не менее 4 часов, при условии, что суперскалярный процессор ограничен зависимостями регистров. Умножение обычно занимает 10 или меньше тактов на современных процессорах, и если эта последовательность становится длиннее, чем во времени, вы могли бы также сделать умножение.
Умножить на 9:
mov eax,edx ; same effect as xor eax,eax/shl edx 1/add eax,edx
shl edx,3 ; x*2^3
add eax,edx
Это бьет умножение; должно занять всего 2 часа.
Что менее известно, так это использование инструкции LEA (эффективный адрес загрузки) для быстрого умножения на маленькую константу. LEA, который занимает только один такт в худшем случае, его время выполнения часто может перекрываться с другими инструкциями суперскалярных процессоров.
LEA, по сути, "добавляет два значения с небольшими постоянными множителями". Он вычисляет t=2^k*x+y для k=1,2,3 (см. Справочное руководство Intel) для t, x и y - любой регистр. Если x==y, вы можете получить 1,2,3,4,5,8,9 раз x, но использование x и y в качестве отдельных регистров позволяет комбинировать промежуточные результатыи перемещать их в другие регистры (например, в t), и это оказывается удивительно удобным. Используя его, вы можете выполнить умножение на 9, используя одну инструкцию:
lea eax,[edx*8+edx] ; takes 1 clock
Тщательно используя LEA, вы можете умножить на множество своеобразных констант за небольшое количество циклов:
lea eax,[edx*4+edx] ; 5 * edx
lea eax,[eax*2+edx] ; 11 * edx
lea eax,[eax*4] ; 44 * edx
Чтобы сделать это, вы должны разложить ваш постоянный множитель на различные факторы / суммы, включающие 1,2,3,4,5,8 и 9. Примечательно, сколько маленьких констант вы можете сделать это, и все еще использовать только 3-4 инструкции.
Если вы разрешаете использовать другие обычно одночасовые инструкции (например, SHL/SUB/NEG/MOV), вы можете умножить на некоторые постоянные значения, которые чистый LEA не может сделать так же эффективно сам по себе. Умножить на 31:
lea eax,[4*edx]
lea eax,[8*eax] ; 32*edx
sub eax,edx; 31*edx ; 3 clocks
Соответствующая последовательность LEA длиннее:
lea eax,[edx*4+edx]
lea eax,[edx*2+eax] ; eax*7
lea eax,[eax*2+edx] ; eax*15
lea eax,[eax*2+edx] ; eax*31 ; 4 clocks
Выяснить эти последовательности немного сложно, но вы можете организовать организованную атаку.
Поскольку LEA, SHL, SUB, NEG, MOV - это наихудший случай однократных команд и нулевые часы, если они не зависят от других команд, вы можете рассчитать стоимость выполнения любой такой последовательности. Это означает, что вы можете реализовать алгоритм динамического программирования для генерации наилучшей возможной последовательности таких инструкций. Это полезно только в том случае, если счетчик тактов меньше целочисленного умножения для вашего конкретного процессора (я использую 5 часов в качестве практического правила), и он не использует все регистры или, по крайней мере, не использует регистры которые уже заняты (избегая любых разливов).
Я фактически встроил это в наш компилятор PARLANSE, и он очень эффективен для вычисления смещений в массивах структур A[i], где размер элемента структуры в A является известной константой. Умный человек, возможно, кеширует ответ, поэтому его не нужно пересчитывать каждый раз, когда происходит умножение одной и той же константы; На самом деле я этого не делал, потому что время генерации таких последовательностей меньше, чем вы ожидаете.
Мягко интересно напечатать последовательности команд, необходимые для умножения на все константы от 1 до 10000. Большинство из них можно выполнить в 5-6 инструкциях в худшем случае. Как следствие, компилятор PARLANSE вряд ли когда-либо использует фактическое умножение при индексации даже самых неприятных массивов вложенных структур.
Если ваши умножения не являются достаточно упрощенными, add
скорее всего, не выиграют mul
, Сказав это, вы можете использовать add
делать умножения:
Multiply by 2:
add eax,eax ; x2
Multiply by 4:
add eax,eax ; x2
add eax,eax ; x4
Multiply by 8:
add eax,eax ; x2
add eax,eax ; x4
add eax,eax ; x8
Они хорошо работают на двоих. Я не говорю, что они быстрее. Они, безусловно, были необходимы в дни, предшествовавшие сложным инструкциям умножения. Это от кого-то, чья душа была выкована в адских пожарах, таких как Mostek 6502, Zilog z80 и RCA1802:-)
Вы можете даже умножить на не-полномочия, просто сохранив промежуточные результаты:
Multiply by 9:
push ebx ; preserve
push eax ; save for later
add eax,eax ; x2
add eax,eax ; x4
add eax,eax ; x8
pop ebx ; get original eax into ebx
add eax,ebx ; x9
pop ebx ; recover original ebx
Я обычно советую писать код в первую очередь для удобства чтения и беспокоиться о производительности только тогда, когда вам это нужно. Однако, если вы работаете в ассемблере, вы, возможно, уже в этот момент. Но я не уверен, что мое "решение" действительно применимо к вашей ситуации, поскольку у вас есть произвольный множитель.
Однако вы всегда должны профилировать свой код в целевой среде, чтобы гарантировать, что то, что вы делаете , действительно быстрее. Ассемблер не меняет этот аспект оптимизации вообще.
Если вы действительно хотите увидеть более универсальный ассемблер для использования add
чтобы сделать умножение, вот процедура, которая примет два значения без знака в ax
а также bx
и вернуть товар в ax
, Он не справится с переполнением элегантно.
START: MOV AX, 0007 ; Load up registers
MOV BX, 0005
CALL MULT ; Call multiply function.
HLT ; Stop.
MULT: PUSH BX ; Preserve BX, CX, DX.
PUSH CX
PUSH DX
XOR CX,CX ; CX is the accumulator.
CMP BX, 0 ; If multiplying by zero, just stop.
JZ FIN
MORE: PUSH BX ; Xfer BX to DX for bit check.
POP DX
AND DX, 0001 ; Is lowest bit 1?
JZ NOADD ; No, do not add.
ADD CX,AX
NOADD: SHL AX,1 ; Shift AX left (double).
SHR BX,1 ; Shift BX right (integer halve, next bit).
JNZ MORE ; Keep going until no more bits in BX.
FIN: PUSH CX ; Xfer product from CX to AX.
POP AX
POP DX ; Restore registers and return.
POP CX
POP BX
RET
Это опирается на тот факт, что 123
умножается на 456
идентичен:
123 x 6
+ 1230 x 5
+ 12300 x 4
это так же, как вас учили умножению еще в классе / начальной школе. Это проще с двоичным, так как вы умножаете только на ноль или единицу (другими словами, добавление или не добавление).
Это довольно старая школа x86 (8086, из сессии DEBUG - я не могу поверить, что они все еще включают эту штуку в XP), так как это было в последний раз, когда я программировал непосредственно на ассемблере. Есть что-то, что можно сказать о языках высокого уровня:-)
Когда дело доходит до инструкции по сборке, скорость выполнения любой инструкции измеряется с использованием тактового цикла. Инструкция Mul всегда занимает больше тактов, чем операция добавления, но если вы выполняете ту же самую инструкцию добавления в цикле, то общий тактовый цикл для умножения с использованием инструкции добавления будет намного больше, чем одиночной инструкции mul. Вы можете взглянуть на следующий URL, который говорит о тактовом цикле одиночной инструкции add / mul. Таким образом, вы можете выполнять математику, которая будет быстрее.
http://home.comcast.net/~fbui/intel_a.html
http://home.comcast.net/~fbui/intel_m.html
Я рекомендую использовать инструкцию mul вместо того, чтобы помещать add в цикл, позднее это очень неэффективное решение.
Я должен повторить ответы, которые у вас уже есть - для общего умножения вам лучше всего использовать MUL - в конце концов, это то, что он там!
В некоторых конкретных случаях, когда вы знаете, что вам захочется умножаться на определенное фиксированное значение каждый раз (например, при разработке индекса пикселя в растровом изображении), вы можете рассмотреть возможность разбить умножение на (маленькую) горстку SHL и ADDs - например:
Дисплей 1280 x 1024 - каждая строка на дисплее имеет 1280 пикселей.
1280 = 1024 + 256 = 2 ^ 10 + 2 ^ 8
y * 1280 = y * (2 ^ 10) + y * (2 ^ 8) = ДОБАВИТЬ (SHL y, 10), (SHL y, 8)
... учитывая, что графическая обработка, вероятно, должна быть быстрой, такой подход может сэкономить вам драгоценные такты.