Masm汇编8086数据字加法之间进位标志

Masm assembly 8086 carry flag between data word addition

所以我有一个我应该解决的问题,我花了几个小时试图找出最好的方法,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
;---------------------------------------

您必须保存上一次迭代的进位标志。

试试这个:

;---------------------------------------
; 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 因为 cmpsub 除了 cmp 仅修改标志而不存储计算值,因此您可以检查直接sub的结果。

OP 说他想要一个低成本的解决方案来保留迭代之间的进位。 @MikeCAT 有 a 解决方案; @PeterCordes 提出了一些改进建议。

在假设您的多精度值为 "big"(包含许多字值)的情况下,您在进行多精度算术时还有一个非常好的改进,那就是展开内部循环 N 次,避免循环counting/carry 标记展开部分内的损坏。 (如果你的多精度算法不是很多重,你不需要很多优化)。

我在这里修改了@MikeCAT 的回答,假设展开应该是 8 次迭代。

代码分为 3 个部分:确定如何处理 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

适当调整代码,留给reader.

是我写的循环还是LODS/STOS版本更快,需要仔细衡量。

如果您想要快速的多精度加法,请尽可能使用 64 位代码。对每条指令执行 4 倍的宽度直接提供 4 倍的加速。在兼容 386 的 CPU 上,您甚至可以在 16 位模式下使用 32 位指令和寄存器,这将使您获得 2 倍的加速。


进一步采用 Ira 的展开建议

  • 将循环开销减少一 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

这应该 运行 在 Intel Broadwell 和 AMD K8 通过 Bulldozer 上几乎每个时钟 adc(减去循环开销)。 (我忘记了 k8/k10 是否可以每个时钟执行两次加载。如果不能,那将是瓶颈)。根据 Agner Fog's tables,将 adc 与内存目标一起使用在 Intel 上不好,但在 AMD 上没问题。 Intel Haswell 及更早版本将受到 adc 的 2c 延迟的限制。 (Broadwell 制作了 adccmov 单 uop 指令,利用 Haswell 中添加的 3 依赖 uop 支持,因此 FMA 可以是单 uop 指令)。

A loop insn 可能会在旧 CPU 上取得胜利,其中部分寄存器停顿确实很糟糕,但避免部分标志停顿的其他方法可能比 the slow loop instruction 更好。

使用 dest-source 技巧可以减少递减指针的循环开销。负载中的 2 寄存器寻址模式不需要微熔断,因为纯 mov 负载无论如何都是单个 uop。商店确实需要微型保险丝,因此您应该更喜欢商店的单寄存器寻址模式。 Haswell 在端口 7 上的额外存储地址单元只能处理简单的寻址模式,并且 2-register addressing modes can't micro-fuse.

另请参阅 ,了解有关多精度 adc 循环的信息以及针对部分标志停顿性能在 Core2 与 SnB CPU 上进行的一些实验。

另一种循环方式是 lea si, -1[si] / mov cx, si / jcxz。 16位很烂,你不能在有效地址中使用[cx]