Сборка Masm 8086 переносит флаг между добавлением слова данных

Итак, у меня есть эта проблема, которую я должен решить, и я потратил часы, пытаясь найти лучший способ сделать это, Google не очень помог.

Проблема состоит в том, чтобы создать подпрограмму, которой дается список слов, который вы затем добавляете к другому списку, который становится результатом. Это в основном метод работы с большими числами.

Мой код отлично работает для флагов переноса внутри слов, но для флагов переноса из одного полного слова в другое он не работает. Первое 16-битное слово (0005 в приведенном ниже примере) — это флаг, используемый для сообщения моей подпрограмме, сколько слов имеется.

Например, учитывая следующие входные данные,

//si     0005 0000 EEEE DDDD CCCC BBBB
//di     0005 0000 1111 2222 3333 4445

когда ожидаемый результат:

0005 0001 0000 0000 0000 0000

Мой код производит:

0005 0000 FFFF FFFF FFFF 0000 

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

;---------------------------------------
; ADD Subroutine
;---------------------------------------
    .data

    bxx dw 0000h                        ;
    cxx dw 0000h                        ;

    .code
;---------------------------------------
addx:                                   ;
    mov bxx, bx                         ;save incoming register
    mov cxx, cx                         ;save incoming register
    mov bx, si                          ;move n to bl - acts as a cursor
loopAdd:                                ;loop point
    mov cx, [si+bx]                     ;move word at point si+bx into register cx
    ADC [di+bx], cx                     ;add with carry  
    sub bx, 0002h;                      ;decrement cursor by a full word
    cmp bx, 0000h                       ;bx == 0?
    jg loopAdd                          ;no? jump to loop point
end:                                    ;
    mov bx, bxx                         ;return register to original state
    mov cx, cxx                         ;return register to original state
    ret                                 ;return
;---------------------------------------

person Harrison    schedule 03.03.2016    source источник
comment
Остаток: cmp изменит флаг CF, а mov нет.   -  person MikeCAT    schedule 03.03.2016


Ответы (3)


Вы должны сохранить флаг переноса из предыдущей итерации.

Попробуй это:

;---------------------------------------
; ADD Subroutine
;---------------------------------------
    .data

    bxx dw 0000h                        ;
    cxx dw 0000h                        ;

    .code
;---------------------------------------
addx:                                   ;
    mov bxx, bx                         ;save incoming register
    mov cxx, cx                         ;save incoming register
    mov bx, si                          ;move n to bl - acts as a cursor
    clc                                 ;clear carry flag
    pushf                               ;save flag register
loopAdd:                                ;loop point
    mov cx, [si+bx]                     ;move word at point si+bx into register cx
    popf                                ;restore saved flag register
    ADC [di+bx], cx                     ;add with carry
    pushf                               ;save flag register
    sub bx, 0002h;                      ;decrement cursor by a full word
    jg loopAdd                          ;if the result is positive, jump to loop point
end:                                    ;
    popf                                ;remove saved flag register from the stack
    mov bx, bxx                         ;return register to original state
    mov cx, cxx                         ;return register to original state
    ret                                 ;return
;---------------------------------------

Обратите внимание, что cmp bx, 0000h не нужен, потому что cmp это sub, за исключением того, что cmp изменяет только флаги и не сохраняет вычисленное значение, поэтому вы можете напрямую проверить результат sub.

person MikeCAT    schedule 03.03.2016
comment
pushf/popf - действительно неуклюжее решение. Использование lea bx, [bx-2] / mov cx, bx / jcxz для зацикливания без влияния на флаги было бы быстрее, или используйте инструкцию loop (даже если она медленная). 16-битные эффективные адреса не позволяют масштабировать индекс, IIRC, в противном случае вы также можете зацикливаться с dec (что не влияет на флаг переноса), но это вызывает остановку или замедление частичного флага на большинстве процессоров. (Гораздо серьезнее для пред-Sandybridge.) lahf/sahf — еще один вариант: обе эти инструкции выполняются быстро на Intel (но немного медленнее на AMD). - person Peter Cordes; 03.03.2016
comment
Спасибо за вашу помощь! - person Harrison; 04.03.2016

Если вам нужно быстрое сложение с множественной точностью, по возможности используйте 64-битный код. Увеличение ширины в 4 раза с каждой инструкцией напрямую дает 4-кратное ускорение. На 386-совместимых процессорах вы можете использовать 32-битные инструкции и регистры даже в 16-битном режиме, что даст вам двукратное ускорение.


Чтобы сделать шаг вперед в предложении Иры по развертыванию

  • сокращение накладных расходов цикла на один lea
  • избегая adc с назначением памяти, что медленно на Intel.

    ... set up for the unrolled loop, with Ira's setup code
    ; di = dst pointer
    ; bx = src-dst, so bx+di = src
add_8words: ; carry bit has value to propagate
    ;sahf   ; another way to avoid a partial-flag stall while looping
    mov  ax, 0[bx+di]
    adc  ax, 0[di]
    mov  0[di], ax
    mov  ax, -1[bx+di]
    adc  ax, -1[di]
    mov  -1[di], ax
    mov  ax, -2[bx+di]
    adc  ax, -2[di]
    mov  -2[di], ax
    mov  ax, -3[bx+di]
    adc  ax, -3[di]
    mov  -3[di], ax
    mov  ax, -4[bx+di]
    adc  ax, -4[di]
    mov  -4[di], ax
    mov  ax, -5[bx+di]
    adc  ax, -5[di]
    mov  -5[di], ax
    mov  ax, -6[bx+di]
    adc  ax, -6[di]
    mov  -6[di], ax
    mov  ax, -7[bx+di]
    adc  ax, -7[di]
    mov  -7[di], ax

    lea   di, -8[di]
    ; lahf
    ; put the flag-setting insn next to the branch for potential macro-fusion
    dec   cx             ; causes a partial-flag stall, but only once per 8 adc
    jne   add_8word

This should run at nearly one adc per clock (minus loop overhead) on Intel Broadwell, and on AMD K8 through Bulldozer. (I forget if k8/k10 can do two loads per clock. That'll be the bottleneck if it can't). Using adc with a memory destination is not good on Intel, but fine on AMD, according to Agner Fog's tables. Intel Haswell and earlier will be limited by the 2c latency of adc. (Broadwell made adc and cmov single-uop instructions, making use of the 3-dependency uop support added in Haswell so FMA can be a single uop instruction).

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

Использование трюка с приемником-источником уменьшает накладные расходы цикла на уменьшение указателя. Режимы адресации с 2 регистрами в нагрузках не нуждаются в микрофьюзе, потому что чистые mov нагрузки в любом случае являются одной uop. хранилищам необходимо использовать микрофьюзы, поэтому для хранилищ следует предпочесть режимы адресации с одним регистром. Дополнительный адресный блок Haswell на порту 7 может обрабатывать только простые режимы адресации, а режимы адресации с двумя регистрами могут не микропредохранитель.

См. также Проблемы с ADC/SBB и INC/DEC в узких циклах на некоторых ЦП для получения информации о многоточечных циклах АЦП и некоторых экспериментах на процессорах Core2 и SnB для производительности с частичным флагом.

Другим способом зацикливания здесь будет lea si, -1[si] / mov cx, si / jcxz. 16-битный отстой, и вы не можете использовать [cx] в эффективном адресе.

person Peter Cordes    schedule 03.03.2016
comment
О, хороший трюк с индексными регистрами. Старая рука учится новому трюку. - person Ira Baxter; 03.03.2016
comment
Спасибо, что нашли время, чтобы написать все это. Я закончил тем, что использовал инструкции цикла и adc - определенно чему-то научился здесь. Я знаю, что цикл должен быть медленным, но он работал в dosbox. - person Harrison; 04.03.2016
comment
@Harrison: DOSBOX — это чистый эмулятор, верно? Он вообще не запускает ваш код изначально? Он эмулирует 386-совместимый ЦП (или даже P6 с MMX/SSE?), поэтому вы могли бы использовать 32-битный adc для выполнения того же цикла, но с удвоенным объемом данных за итерацию. Все проблемы с обработкой флагов циклов adc идентичны для 16-битных и 32-битных, за исключением того, что вы можете использовать lea для добавления без флагов в любой регистр, который вам нравится. - person Peter Cordes; 05.03.2016
comment
@PeterCordes Я считаю, что это так, да. Я буду иметь это в виду в будущем. Еще раз спасибо за все ваши рекомендации! - person Harrison; 07.03.2016

OP говорит, что ему нужно недорогое решение, чтобы сохранить перенос между итерациями. У @MikeCAT было решение; @PeterCordes предложил некоторые улучшения.

Есть еще одно действительно приятное улучшение, которое вы можете получить при выполнении арифметики с множественной точностью, при условии, что ваше значение с множественной точностью "большое" (содержит много значений слов), и это разворачивает внутренний цикл N раз, избегая повреждения подсчета цикла/переноса флага внутри развернутый раздел. (Если ваша арифметика с множественной точностью не очень кратна, вам не нужна большая оптимизация).

Я пересмотрел здесь ответ @MikeCAT, предполагая, что развертывание должно состоять из 8 итераций.

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

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

[Следующий код не проверен.]

;---------------------------------------
; ADD Subroutine
;   si = pointer to count word of 1st multiprecision value
;   di = pointer to count word of 2nd multiprecision value
;   assert: si->count == di ->count
;   preserves si, di; exits with carry from addition
;---------------------------------------
sizeofword equ 2
;---------------------------------------
add_multiple: ; destroys ax, si, di
    push cx                             ;save incoming register
    mov  cx, [si]
    lea  si, sizeofword[si+cx]          ; find least significant word  
    lea  di, sizeofword[di+cx]

; determine entry point into unrolled loop by taking counter modulo 8
    mov cx, si                          ;move n to bl - acts as a cursor
    shr cl, 1 
    jc  add_xx1
    je  done                            ; edge case: 0 words in value
add_xx0:
    shr cl, 1
    jc  add_x10
add_x00:
    shr cl, 1
    jnc add_000                         ; note carry flag is clear                         
;   clc                                
;   jmp add_100
    mov  ax, 0[si]                      
    add  0[di], ax                      ; do 1st add without carry
    lea  si, -1[si]
    lea  di, -1[di]
    jmp  add_011

add_x10:
    shr cl, 1
    jnc add_010
;   clc
;   jmp add_110
    mov  ax, 0[si]
    add  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
    jmp  add_101

add_x01:
    shr cl, 1
    jnc add_001
;   clc
;   jmp add_101
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
    jmp  add_100

add_xx1:
    shr cl, 1
    jnc add_x01
add_x11:
    shr cl, 1
    jnc add_011
;   clc
;   jmp add_111

; the following code adds a fragment of an 8 word block
add_111: ; carry bit has value to propagate
    mov  ax, 0[si]         
;   adc  0[di], ax
    add  0[di], ax                             ; no carry in on 1st add
    lea  si, -1[si]
    lea  di, -1[di]
add_110:
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
add_101:
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
add_100:
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
add_011:
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
add_010:
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
add_001:
    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]
add_000:
    mov  ax, 0[si]
    adc  0[di], ax
    dec   cx                     ; does not disturb carry
    lea  si, -1[si]
    lea  di, -1[di]
    je    done

; unrolled loop here; very low overhead
add_8words: ; carry bit has value to propagate
    mov  ax, 0[si]
    adc  0[di], ax
    mov  ax, -1[si]
    adc  -1[di], ax
    mov  ax, -2[si]
    adc  -2[di], ax
    mov  ax, -3[si]
    adc  -3[di], ax
    mov  ax, -4[si]
    adc  -4[di], ax
    mov  ax, -5[si]
    adc  -5[di], ax
    mov  ax, -6[si]
    adc  -6[di], ax
    mov  ax, -7[si]
    adc  -7[di], ax
    dec   cx
    lea   si, -8[si]
    lea   di, -8[di]
    jne   add_8word
done: pop  cx
    ret

;---------------------------------------

Последовательность

    mov  ax, 0[si]
    adc  0[di], ax
    lea  si, -1[si]
    lea  di, -1[di]

предлагает, возможно, использовать инструкции перемещения блока из одного слова в качестве альтернативы:

    std                          ; want to step backward
    ...
    lods
    adc  ax, 0[di]
    stos
    ...
    cld
    ret

с соответствующими корректировками кода, оставленными читателю.

Будет ли цикл, который я написал, или версия LODS / STOS быстрее, нужно тщательно измерить.

person Ira Baxter    schedule 03.03.2016
comment
Да, развертывание — это огромная победа с adc, потому что зацикливание обходится дороже, чем обычно. Я пытался сделать это просто. См. также stackoverflow.com/a/32087095/224132, включая комментарии. Я почти уверен, что версия lods/stos будет хуже. lods и stos - это инструкции с 3 операциями или 3 операциями на процессорах Intel и AMD. (lodsd — это 2-оперативная инструкция на Haswell и более поздних версиях, но lodsw по-прежнему 3). Одно хранилище за такт должно быть узким местом пропускной способности, но пропускная способность uop будет узким местом с lods/stos. - person Peter Cordes; 03.03.2016
comment
Однако adc m, r/i составляет 4 моп с пропускной способностью 1 на 2 с, даже на Broadwell/Skylake, где adc r, r/i — это 1 моп с задержкой 1 с. (Haswell и более ранние версии имеют задержку 2c adc, потому что uop не может иметь более двух входных зависимостей. Haswell имел FMA; Broadwell расширил эту функциональность uop с 3 входами до adc. всегда 2 моп. Тем не менее, на Haswell и более ранних версиях adc r,m и adc r,r оба 2 моп, поэтому загрузка/adc r,m/store (с инструкциями mov), вероятно, оптимальна: 4 моп на adc, насыщая внешний интерфейс, где adc имеет задержку 1c . - person Peter Cordes; 03.03.2016
comment
На Intel до Broadwell вы фактически будете ограничены задержкой adc до половины этой пропускной способности. Кроме того, никто не упомянул слона в комнате: 64-битный код будет в 4 раза быстрее, потому что каждая 64-битная adc может выполнять работу четырех 16-битных adc инструкций. - person Peter Cordes; 03.03.2016
comment
Я только что опубликовал это как ответ. - person Peter Cordes; 03.03.2016
comment
Я остановился на 8086, потому что так его поставил OP. Я согласен, 64-битная версия была бы быстрее. (Это много отличных данных о стоимости обучения). - person Ira Baxter; 03.03.2016
comment
Спасибо, что нашли время ответить на мой вопрос! Я узнал из ваших утверждений lea и в конечном итоге использовал их в своем окончательном решении. - person Harrison; 04.03.2016