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
因为 cmp
是 sub
除了 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 制作了 adc
和 cmov
单 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]
。
所以我有一个我应该解决的问题,我花了几个小时试图找出最好的方法,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
因为 cmp
是 sub
除了 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 制作了 adc
和 cmov
单 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.
另请参阅
另一种循环方式是 lea si, -1[si]
/ mov cx, si
/ jcxz
。 16位很烂,你不能在有效地址中使用[cx]
。