【问题标题】:Masm assembly 8086 carry flag between data word additionMasm 汇编 8086 数据字添加之间的进位标志
【发布时间】:2016-03-03 07:08:42
【问题描述】:

所以我有这个我应该解决的问题,我花了几个小时试图找出最好的方法来解决这个问题,谷歌并没有提供太多帮助。

问题是创建一个子例程,它给出一个单词列表,然后添加另一个列表作为输出。它基本上是一种处理大量数字的方法。

我的代码对于携带标志 within 单词工作正常,但对于从一个完整单词到另一个单词的携带标志,它不起作用。第一个 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
;---------------------------------------

【问题讨论】:

  • 剩余:cmp 将修改标志 CFmov 不会。

标签: assembly masm word carryflag


【解决方案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
    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的结果。

【讨论】:

  • pushf/popf 是一个非常笨拙的解决方案。使用lea bx, [bx-2] / mov cx, bx / jcxz 在不影响标志的情况下循环会更快,或者使用loop 指令(即使它很慢)。 16 位有效地址不允许缩放索引 IIRC,否则您也可以使用 dec 循环(这不会影响进位标志),但这会导致大多数 CPU 出现部分标志停止或减速。 (在 Sandybridge 之前的版本要严重得多。)lahf/sahf 是另一种选择:这些指令在 Intel 上都很快(但在 AMD 上有点慢)。
  • 感谢您的帮助!
【解决方案2】:

如果您想要快速的多精度加法,请尽可能使用 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 上很好。英特尔 Haswell 及更早版本将受到 adc 的 2c 延迟的限制。 (Broadwell 制作了 adccmov 单微指令,利用 Haswell 中添加的 3 依赖微指令支持,因此 FMA 可以是单微指令)。

loop insn 可能会在旧 CPU 上胜出,因为在旧 CPU 上,部分寄存器停顿非常糟糕,但避免部分标志停顿的其他方法可能比 the slow loop instruction 更好。

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

另请参阅Problems with ADC/SBB and INC/DEC in tight loops on some CPUs,了解有关多精度 adc 循环的信息以及关于 Core2 与 SnB CPU 的部分标志停顿性能的一些实验。

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

【讨论】:

  • 哦,索引寄存器的妙招。老手学会新把戏。
  • 感谢您花时间写下所有这些。我最终使用了循环和 adc 指令——在这里肯定学到了一些东西。我知道循环应该很慢,但这是在 dosbox 中运行的。
  • @Harrison:DOSBOX 是一个纯模拟器,对吧?它根本不运行您的代码吗?它确实模拟了兼容 386 的 CPU(甚至是带有 MMX/SSE 的 P6?),因此您可以使用 32 位 adc 来执行相同的循环,但每次迭代的数据是两倍。 adc 循环的所有标志处理问题在 16 位和 32 位之间是相同的,除了您可以使用lea 来在您喜欢的任何寄存器上添加无标志。
  • @PeterCordes 我相信是这样,是的。以后我会牢记这一点。再次感谢您的指导!
【解决方案3】:

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

假设您的多精度值“大”(包含许多单词值),并且将内部循环展开 N 次,避免循环计数/进位标志,在进行多精度算术时,您可以获得另一项非常好的改进展开部分内部的损坏。 (如果你的多精度算术不是非常多,你不需要很多优化)。

我在这里修改了@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

对代码进行适当的调整,留给读者。

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

【讨论】:

  • 是的,adc 的展开是一个巨大的胜利,因为循环比正常情况更昂贵。我试图保持简单。另请参阅 stackoverflow.com/a/32087095/224132,包括 cmets。我很确定lods / stos 版本会更糟。 lods 和 stos 都是 Intel 和 AMD CPU 上的 3-uop 或 3-mop 指令。 (lodsd 是 Haswell 及更高版本上的 2 uop 指令,但 lodsw 仍为 3)。每个时钟一个存储应该是吞吐量瓶颈,但 uop 吞吐量将是 lods/stos 的瓶颈
  • 但是,adc m, r/i 是 4 uop,每 2c 吞吐量一个,即使在 Broadwell / Skylake 上 adc r, r/i 是一个 uop,延迟为 1c。 (Haswell 和更早的版本有 2c 延迟 adc,因为 uop 不能有两个以上的输入依赖项。Haswell 有 FMA;Broadwell 将 3 输入 uop 功能扩展到 adc。而 adc r, m 不能微在 BDW 上熔断,所以它总是 2 微指令。然而,在 Haswell 和更早的版本上,adc r,madc r,r 都是 2 微指令。所以 load/adc r,m/store(使用 mov 指令)可能是最佳的:4每 adc 的 uops,使 adc 延迟为 1c 的前端饱和。
  • 在 Intel 之前的 Broadwell 上,您实际上会受到 adc 延迟的限制,只有一半的吞吐量。此外,没有人提到房间里的大象:64 位代码会快 4 倍,因为每个 64 位 adc 可以完成四个 16 位 adc 指令的工作。
  • 我刚刚发布了这个作为答案。
  • 我坚持使用 8086,因为这就是 OP 提出的方式。我同意,64 位版本会更快。 (关于教学成本的大量数据)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-11-26
  • 2011-08-06
  • 2017-02-09
  • 1970-01-01
  • 2011-12-23
  • 2015-04-10
  • 2013-10-18
相关资源
最近更新 更多