如何在 IA32 上将带符号的整数相加成更大的和。 32位有符号整数的64位总和?

Posted

技术标签:

【中文标题】如何在 IA32 上将带符号的整数相加成更大的和。 32位有符号整数的64位总和?【英文标题】:How to sum integers with sign into a wider sum on IA32. 64-bit sum of 32-bit signed ints? 【发布时间】:2017-11-05 17:54:48 【问题描述】:

我正在尝试对 Assembly 32 上带有符号的整数列表求和,但我只需要对没有符号的整数求和。你知道有什么方法吗?

我的程序尝试对整数求和并存储在 resultado 中,其大小为 64 位,因此我使用两个 32 位寄存器(EAX 和 EDX),并检查总和何时产生进位.

之后,我在 resultado 上加入了 EAX 和 EDX。

# sum.s     Sumar los elementos de una lista.
#           llamando a función, pasando argumentos mediante registros
# retorna:  código retorno 0, comprobar suma en %eax mediante gdb/ddd.
# as --32 -g sum.s -o sum.o
# ld -m elf_i386 sum.o -o sum

# DATA SECTION
.section .data
lista:
    .int 4294967295, 4294967295, 4294967295, 4294967295

longlista:
    .int (.-lista)/4
resultado:
    .quad -1


.section .text
_start: .global _start

    mov $lista, %ebx
    mov longlista, %ecx
    call suma
    mov %eax, resultado
    mov %edx, resultado+4

    mov $1, %eax
    mov $0, %ebx
    int $0x80


suma:
    push %esi
    mov $0, %eax
    mov $0, %edx
    mov $0, %esi

bucle:
    add (%ebx,%esi,4), %eax
    jc .L1

bucle1:
    inc %esi
    cmp %esi,%ecx
    jne bucle
    pop %esi
    ret

.L1:
    inc %edx
    jmp bucle1

这给出了一个 64 位和,将输入视为无符号 32 位,这不是我想要的。

【问题讨论】:

签名也一样。你真的有这个问题吗? @Jester:实际上这段代码是将 32 位整数零扩展为 64 位,而不是符号扩展。 (当总和与数组元素的宽度相同时,它是相同的,但这不是 OP 在这里所做的)。 啊,错过了。 @Antonio:在 x86-64 上更简单:movsxd (%rbx,%rsi,4), %rdx / add %rdx, %rax 签署扩展。正如 Sep 所说,在 x86-32 上,将符号扩展到一对寄存器的最简单方法是使用 cdqeax 扩展到 edx:eax 【参考方案1】:

下一个使用 64 位加法的代码将为正数和负数提供正确的总和,而不会由于仅使用 32 位寄存器而产生任何回绕。 签名结果可以超出范围 [-2GB,+2GB-1]。

suma:
    push %esi
    push %edi
    xor  %esi, %esi           ;Clear %edi:%esi
    xor  %edi, %edi
    sub  $1, %ecx             ;Start at last element in array
    jl   emptyArray
bucle:
    mov  (%ebx,%ecx,4), %eax  ;From signed 32-bit to signed 64-bit
    cdq
    add  %eax, %esi           ;Add signed 64-bit numbers
    adc  %edx, %edi
    dec  %ecx
    jge  bucle
emptyArray:
    mov  %esi, %eax           ;Move result from %edi:%esi to %edx:%eax
    mov  %edi, %edx
    pop  %edi
    pop  %esi
    ret

添加的顺序并不重要,因此代码从最后一个元素开始,向第一个元素靠拢。

【讨论】:

如果您要在零处跳出循环,请安排好它,这样您就不需要test 指令。 循环中的指令较少是循环到零点的一部分。例如dec %ecx 循环前一次,然后dec %ecx / jge 将循环计数器视为有符号,当ecx = 0 时循环运行一次,当它变为-1 时停止。 (或者在寻址模式中使用位移)。顺便说一句,您不需要保存/恢复%ecx; OP 似乎使用了一些正常的调用约定,其中通常的寄存器被调用破坏。【参考方案2】:

您当前的代码隐式零扩展。它相当于add (%ebx,%esi,4), %eax / adc $0, %edx,但你需要添加到上半部分的是0 或-1,具体取决于下半部分的符号。 (即符号位的 32 个副本;请参阅 Sep 的答案)。


32 位 x86 可以直接使用 SSE2/AVX2/AVX512 paddq 进行 64 位整数数学运算。 (所有支持 64 位的 CPU 都支持 SSE2,所以现在这是一个合理的基准)。

(或 MMX paddq 如果您关心 Pentium-MMX 到 Pentium III / AMD Athlon-XP)。

SSE4.1 使符号扩展到 64 位变得便宜。

pmovsxdq  (%ebx),  %xmm1     # load 2x 32-bit (Dword) elements, sign-extending into Qword elements
paddq     %xmm1, %xmm0
add       $8, %ebx

cmp / jb             # loop while %ebx is below an end-pointer.
# preferably unroll by 2 so there's less loop overhead,
# and so it can run at 2 vectors per clock on SnB and Ryzen.  (Multiple shuffle units and load ports)

# horizontal sum
pshufd    $0b11101110, %xmm0, %xmm1    # xmm1 = [ hi | hi ]
paddq     %xmm1, %xmm0                 # xmm0 = [ lo + hi | hi + hi=garbage ]

# extract to integer registers or do a 64-bit store to memory.
movq      %xmm0, (result)

我避免了索引寻址模式so the load can stay micro-fused with pmovsxdq on Sandybridge。在 Nehalem、Haswell 或更高版本,或者在 AMD 上,索引很好。


不幸的是,没有 SSE4.1 的 CPU 仍在使用中。在这种情况下,您可能只想使用标量,但您可以手动进行符号扩展。

不过,没有 64 位算术右移。 (仅 64 位元素大小的逻辑移位)。但是您可以通过复制并使用 32 位移位来广播符号位,然后解包来模拟 cdq

# prefer running this on aligned memory
# Most CPUs without SSE4.1 have slow movdqu

.loop:
    movdqa    (%ebx, %esi, 1), %xmm1      # 4x 32-bit elements
    movdqa    %xmm1, %xmm2
    psrad     $31, %xmm1                  # xmm1 = high halves (broadcast sign bit to all bits with an arithmetic shift)

    movdqa    %xmm2, %xmm3               # copy low halves again before destroying.
    punpckldq %xmm1, %xmm2                # interleave low 2 elements -> sign-extended 64-bit
    paddq     %xmm2, %xmm0

    punpckhdq %xmm1, %xmm3                # interleave hi  2 elements -> sign-extended 64-bit
    paddq     %xmm3, %xmm0

    add       $16, %esi
    jnc   .loop            # loop upward toward zero, with %ebx pointing to the end of the array.
    #end of one loop iteration, does 16 bytes

(使用两个独立的向量累加器可能比在xmm0 中使用两个paddq 更好,以保持依赖链更短。)

这是更多指令,但每次迭代执行的元素数量是原来的两倍。每个 paddq 的指令仍然更多,但它可能仍然比标量更好,尤其是在 Broadwell 之前的 Intel CPU 上,adc 是 2 微指令(因为它有 3 个输入:2 个寄存器 + EFLAGS)。

在第一个 psrad 之前复制两次 %xmm1 可能会更好。在 movdqa 具有非零延迟的 CPU 上,我想复制并使用原始代码来缩短关键路径,以便乱序执行具有更少的延迟隐藏。

但这意味着最后一个 punpck 正在读取 2x movdqa 寄存器副本链的结果。这在CPUs with mov-elimination that doesn't work 100% of the time (Intel) 上可能会更糟。它可能需要一个向量 ALU 来进行复制,因为 mov 寄存器副本链是 mov-elimination 无法完美运行的情况之一。

【讨论】:

以上是关于如何在 IA32 上将带符号的整数相加成更大的和。 32位有符号整数的64位总和?的主要内容,如果未能解决你的问题,请参考以下文章

excel表格中如何使随机数相加成一个固定值?

带无符号整数的 QSpinBox 用于十六进制输入

C中的无符号整数在java中的处理

IA32的三种地址

从蓝牙低功耗 GATT 特性中检索大的 32 位无符号整数

你如何在 SSE2 上进行带符号的 32 位扩展乘法?