добавить против мул (IA32-Assembly) - PullRequest
5 голосов
/ 14 сентября 2010

Я знаю, что add быстрее по сравнению с функцией mul .

Я хочу знать, как использовать 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 ]

12 голосов
/ 14 сентября 2010

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

Вы не можете всерьез использовать add , чтобы заставить этот код работать быстрее, чем с mul . Если вам нужно умножить на какое-то небольшое постоянное значение (например, 2), то, возможно, вы могли бы использовать add , чтобы ускорить процесс. Но для общего случая - нет.

9 голосов
/ 14 сентября 2010

Если вы умножаете два значения, которые вы не знаете заранее, фактически невозможно выполнить инструкцию умножения в ассемблере 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 в качестве отдельных регистров позволяет объединять промежуточные результаты и , перемещаемые в другиерегистры (например, к т), и это оказывается очень удобно.Используя его, вы можете выполнить умножение на 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 вряд ли когда-либо использует фактическое умножение при индексации даже самых неприятных массивов вложенных структур.

4 голосов
/ 14 сентября 2010

Если ваши умножения не являются достаточно простыми, 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), так как это было в последний раз, когда я кодировал непосредственно на ассемблере. Что-то нужно сказать о языках высокого уровня: -)

3 голосов
/ 14 сентября 2010

Когда дело доходит до инструкции по сборке, скорость выполнения любой инструкции измеряется с использованием тактового цикла.Инструкция mul всегда занимает больше тактов, чем операция добавления, но если вы выполняете ту же самую инструкцию добавления в цикле, то общий тактовый цикл для умножения с использованием инструкции add будет намного больше, чем одиночная команда mul.Вы можете взглянуть на следующий URL, который говорит о тактовом цикле одной инструкции add / mul. Таким образом, вы можете выполнять математику, которая будет быстрее.

http://home.comcast.net/~fbui/intel_a.html#add

http://home.comcast.net/~fbui/intel_m.html#mul

Моя рекомендация состоит в том, чтобы использовать инструкцию mul вместо того, чтобы помещать add в цикл, последующее является очень неэффективным решением.

0 голосов
/ 14 сентября 2010

Мне бы пришлось повторить ответы, которые у вас уже есть - для общего умножения вам лучше всего использовать MUL - в конце концов, это то, что нужно!

В некоторых конкретных случаях, когда вы знаете, что вам нужно будет умножаться на определенное фиксированное значение каждый раз (например, при обработке индекса пикселя в растровом изображении), тогда вы можете рассмотреть разрыв умножить на (маленькую) горстку SHL и ADD - например:

1280 x 1024 дисплей - каждая строка на дисплей 1280 пикселей.

1280 = 1024 + 256 = 2 ^ 10 + 2 ^ 8

y * 1280 = y * (2 ^ 10) + y * (2 ^ 8) = ADD (SHL y, 10), (SHL y, 8)

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...