两个数组的和,每个数组 n 个字节

Posted

技术标签:

【中文标题】两个数组的和,每个数组 n 个字节【英文标题】:the sum of two arrays n bytes each 【发布时间】:2017-08-26 22:08:43 【问题描述】:

这是我的解决方案,我想知道它是否正确以及解决它的另一种方法是什么

include 'emu8086.inc'
org 100h

mov CX,n
lea SI,a
mov AL,0

start_sum_a:         ;sum all the n elements of the first array
add AL, [SI] 
inc SI
loop start_sum_a

mov CX,n
lea SI,b 

start_sum_b:        ;sum all the n elements of the 2nd array to 
add AL, [SI]        ;the first sum
inc SI
loop start_sum_b

call print_num      ;print the sum

ret

a db 1,3,5,7,9,11,13,15,17,18
b db 0,2,4,6,8,10,12,14,16,19
n dw 10

DEFINE_PRINT_NUM
DEFINE_PRINT_NUM_UNS

【问题讨论】:

对不起,我忘了在解决方案和我的陈述之间进行切换 你的总和应该在 255 处环绕,还是应该在添加到 16 位累加器之前进行零扩展?是的,当然还有其他方法可以解决它。例如,对于 SSE2,将所有 16 个字节加载到一个 xmm 寄存器中,并使用pxor xmm1,xmm1 / psadbw xmm0, xmm1。请参阅***.com/questions/10932550/… 以获得完整的解决方案。 仍然使用 8086(其中 SSE 甚至 32 位寄存器不可用),如果您知道您的总和不会溢出一个字节,您可以与 add ax, [si] 并行处理 2 个字节,最后是add al, ah。您也可以使用SI 作为循环条件,而不是仅使用 CX 作为计数器。并且无需将10 存储在内存中;它可能是一个equ 常量。此外,由于您的数组是相邻的,您可以只使用一个循环,该循环从 a 的开头开始并在 b 的结尾结束。 非常感谢您所做的一切努力,这对我帮助很大 【参考方案1】:

我想知道它是否正确

您的解决方案看起来不错,尽管我预感到“emu8086.inc”中定义的函数print_num 会更希望AX 寄存器中的数字。所以最好将mov AL,0 指令更改为xor ax,ax,这将清除整个AX 寄存器,而不仅仅是它的低字节AL

还有什么办法解决

如果您为两个数组设置单独的指针,您可以选择在单个循环中完成工作。

    lea  si, a
    lea  di, b
    mov  cx, n
    xor  ax, ax
start_sum:
    add  al, [si]        ;Element of a array
    add  al, [di]        ;Element of b array
    inc  si
    inc  di
    loop start_sum

但由于这些数组的起点在内存中相距一定距离 (10),因此有一个解决方案:仅使用 1 个指针:

    lea  si, a
    mov  cx, n
    xor  ax, ax
start_sum:
    add  al, [si]        ;Element of a array
    add  al, [si + 10]   ;Element of b array
    inc  si
    loop start_sum

最后,由于这些数组在内存中是相邻的,循环可以更简单。只需将迭代次数加倍(Peter Cordes 的建议之一):

    lea  si, a
    mov  cx, n
    shl  cx, 1           ;Double the counter
    xor  ax, ax
start_sum:
    add  al, [si]        ;Element of a array or b array
    inc  si
    loop start_sum

【讨论】:

或者更好的是,使用alah(或aldl)并在末尾添加,这样您的代码可以在超标量CPU 上运行得更快。仅使用一个累加器展开会失去很多好处。 另外,如果ab 之间的偏移量必须是运行时变量,请将其放入寄存器并使用索引寻址模式。 add al, [si] / add ah, [si + di] 我发布了自己的答案,因为我在 cmets 中写的内容应该首先作为答案发布。 @PeterCordes 它已经成为一个什么样的答案。我无法想象有这么多关于这个谦虚的问题要讲。不错!【参考方案2】:

还有什么办法解决

总是有很多方法可以做任何事情。有些会比其他的更有效,并且有不同的效率衡量标准。不同的效率测量包括代码大小(以指令字节为单位)或小型阵列或大型阵列的性能。对于真正的 8086,代码大小通常是性能的决定因素,但对于现代 x86 CPU,这绝对不是真的。 (请参阅 x86 标签 wiki 以获取文档链接)。

没有必要在内存中存储 10;它应该是一个equ 常量。 IDK 如果您应该假装您正在编写一个不利用所有汇编时常量的函数。如果是这样,那么请注意如何使用常量。 (就像不要写 mov di n + OFFSET a 来在汇编时计算结束指针。)

您可以避免the slow loop instruction,而无需增加循环中的指令数,方法是从数组末尾向下计数索引,并使用索引寻址模式。

另外,由于您的数组是相邻的,您可以只使用一个从 a 开始到 b 结束的循环

mov   bx, OFFSET a          ; no point in using LEA for this
mov   si, length_ab - 1     ; index of the last element
xor   ax,ax

sum_loop:              ; do 
add   al, [bx+si]
dec   si
jg  sum_loop           ;  while(si > 0)

jmp   print_num        ; tailcall optimization: print_num will return directly to our caller
;call print_num
;ret

section .rodata
a:  db 1,3,5,7,9,11,13,15,17,18
b:  db 0,2,4,6,8,10,12,14,16,19
end_b:                   ; put a label after the end of b
length_ab equ $ - a      ; this is NASM syntax, IDK if emu8086 accepts it
n equ 10

或者利用a 是静态的:add AL, [a + SI]。这在真正的 8086 上可能会更慢,因为它在循环中放置了额外的 2 字节代码,8086 每次都必须重新获取。在现代 CPU 上,保存 mov bx, OFFSET a 指令对于总代码大小是值得的。 (如果你在一个循环中多次使用同一个指针,那么将它放在寄存器中是有意义的。)


如果您知道您的总和不会溢出一个字节,您可以与 add ax, [si] 并行处理 2 个字节,最后是 add al, ah。但这绝对是一种特殊情况,处理一般情况(避免进位到下一个字节)with SWAR techniques 仅适用于 2 字节字。在 386 或更高版本的 16 位代码中,您可以使用 32 位寄存器并分别屏蔽奇数和偶数字节。


在某些超标量 CPU(如 Intel pre-Sandybridge,每个时钟周期只能执行一次加载)上,这会更快,允许您在每个时钟添加近 2 个字节:

    xor   ax,ax
    xor   dx,dx
sum_loop:               ; do
    mov   cx, [si]
    add   al, cl
    add   dl, ch

    add   si, 2
    cmp   si, end_a
    jb  sum_loop        ;  while (si < end_pointer)

    add   al, dl
    ;; mov ah,0   ; if necessary

但在其他 CPU 上,最好只展开并使用 add al, [si] / add dl, [si+1] 而不是使用单独的加载指令。

在 Intel P6 和 Sandybridge 系列以外的 CPU 上,alah 不会单独重命名,因此 add ah, ch 会对完整的 ax 寄存器产生错误的依赖性。这就是为什么我使用dl 而不是ah

请注意,至少在现代 Intel CPU (Haswell/Skylake) 上,xor ax,ax 不会破坏依赖关系。它使 AX 为零,但它不会消除对 EAX 旧值的无序执行数据依赖性。见How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent。它在 Sandybridge 和更早的版本上可能会破坏深度,但绝对更喜欢 xor eax,eax 来清零寄存器。


如果您不需要您的代码与过时的 8086 兼容,您可以使用 SSE2 psadbw 只需几个步骤即可完成整个工作。

请参阅Sum reduction of unsigned bytes without overflow, using SSE2 on Intel 了解说明。

您的两个数组共有 20 个字节,因此我们可以对其进行硬编码并将其处理为 16 + 4。

pxor    xmm0, xmm0    ; xmm0 = 0
movd    xmm1, [a+16]  ; load last 4 bytes
psadbw  xmm1, xmm0    ; sum2 = xmm1 = |b[7]-0| + |b[8]-0] + ...
psadbw  xmm0, [a]     ; horizontal sum 16 bytes into 2 partial sums in the two 64-bit halves (sum0 and sum1)

; then combine those three 16-bit sums into a single sum.
paddw   xmm1, xmm0    ; sum2 += sum0
punpckhqdq xmm0, xmm0 ; get the high half of xmm0
paddw   xmm1, xmm0    ; sum2 += sum1
movd    eax, xmm1

movzx    eax, al      ; truncate the sum to 8-bit

jmp    print_num

section .rodata
ALIGN 16          ; having a aligned lets us use [a] as a memory operand, or movdqa
a: ...
b: ...

是的,这将在 16 位模式下组装(例如使用 NASM)。

稍后截断而不是在每一步之后截断对于加法是很好的,因为从低字节回绕或进位是一样的。

如果您不能利用 ab 相邻的优势,您可以:

movdqu  xmm0, [a]
movdqu  xmm1, [b]
paddb   xmm0, xmm1  ; add packed bytes (no carry across byte boundaries)
psrldq  xmm0, 6     ; shift out the high 6 bytes from past the end of a and b

甚至避免读取超出数组末尾的内容:

movq    xmm0, [a]
pinsrw  xmm0, [a+8], 4

我刚刚意识到,由于您显然确实希望将总和包装为 8 位,您可以使用 paddb 来提高效率。对于大数组,你可以用paddb累加,最后做一个psadbw

movd    xmm1, [a+16]  ; load last 4 bytes, zeroing the rest of the register
paddb   xmm1, [a]
pxor    xmm0, xmm0    ; xmm0 = 0
psadbw  xmm1, xmm0    ; horizontal sum one vector of byte-sums

movhlps xmm0, xmm1    ; extract high half into a different register
paddw   xmm0, xmm1    
movd    eax, xmm1     

movzx    eax, al      ; truncate the sum to 8-bit
jmp    print_num

【讨论】:

我不是 SSE 专家,但 movq eax, xmm1 是否正确?我有点期待movd ... @SepRoland:谢谢,已修复。我想我的头在我的手指之前。 (有趣的事实:一些汇编程序接受或可能需要 movd 用于 32 或 64 位 GP XMM 数据移动,并使用 movq 用于 MMX/SSE 加载/存储指令(这是一个单独的操作码)。)

以上是关于两个数组的和,每个数组 n 个字节的主要内容,如果未能解决你的问题,请参考以下文章

c语言实践 创建两个包含8个元素的double类型数组,第二个元素的每个元素的值都是对应前一个元素的前n个元素的和

和为定值的两个数

程序将数组分成N个连续子数组,使每个子数组的和为奇数

一道算法题

2022-01-18:将数组分成两个数组并最小化数组和的差。 给你一个长度为 2 * n 的整数数组。你需要将 nums 分成 两个 长度为 n 的数组,分别求出两个数组的和,并 最小化 两个数组和之

树状数组