【问题标题】:How to compare elements in array in assembly如何在汇编中比较数组中的元素
【发布时间】:2015-12-01 10:49:28
【问题描述】:

如何在汇编语言 x86 中比较数组中的所有元素?我必须比较数组中的所有元素并打印最大的一个

【问题讨论】:

  • 与任何其他语言一样。具体来说,你在哪里卡住了?
  • 我知道答案,但这不是 stackoverflow 的工作方式。请向我们展示您的尝试,我们可能会建议如何解决它。但你必须先尝试。
  • 查看CMPS 指令。可能会有所帮助。
  • SO 不是一个您可以询问“教我如何编程”问题的网站。就像 enhzflep 说的,算法和任何其他语言都是一样的。
  • 通过编写一些代码,组装,链接和执行它。

标签: assembly


【解决方案1】:

我将假装这不是一个可怕的琐碎问题,并实际讨论在汇编语言中执行此操作的有趣部分(而不是让编译器为您优化它)。


在 asm 中,您可以像使用任何其他语言一样进行操作。但是,如果您正在为带有向量指令的机器编程,您可以并且应该使用它们。编译器通常会为您执行此操作,但在 asm 中您必须自己执行此操作。

既然用asm写代码的主要原因是high performance,那么我们来考虑一些问题:


如果没有向量指令,使用条件移动来执行通常的x=max(x, a[i]) 可能是一个好主意,也可能不是一个好主意。 cmov 会引入循环携带的依赖关系,这可能比偶尔的分支错误预测更能损害性能。 (谷歌了解更多信息)。

在找到最大值时,分支错误预测可能并不常见,除非您的值很嘈杂但平均会增加。 (例如,每 1 到 10 个元素就会看到一个新的最大值,这几乎是最坏的情况。)否则,您可能会在很长一段时间内要么总是看到一个新的最大值,要么永远不会看到一个新的最大值。


x86 具有向量 min/max 指令,其工作方式类似于 cmp/cmov 在每个元素的基础上。

因此,如果您的数组由 32 位有符号整数组成,您可以通过将前 4 个元素加载到向量寄存器中来使用 start(例如 xmm0),然后在循环中使用 add rsi, 16 / PMAXSD xmm0, [rsi] 来执行 4 个打包 @987654327 @ 操作。 PMAXSD 英文是:Packed(integer) Max of Signed DWord elements。请参阅 wiki 中的链接以获取参考指南。 PMAXSD 是 SSE4.1 的一部分,因此仅在具有该功能位的 CPU 上受支持。

如果您的数组由uint8_t 元素组成,您将使用PMINUB(Packed(int) Min of Unsigned Byte 元素)。 PMIN/MAXUB 和 PMIN/MAXSW 在 SSE2 中,因此它们是 x86-64 的基准(以及需要足够新硬件和 SSE2 支持的操作系统上的 x86-32)。

遍历数组后(可能使用 PALIGNR 或 PSRLDQ 来处理数组的最后一个非 16B 的倍数位),您的累加器向量中将有 4 个元素。每一个都是每 4 个元素的最大值,用于四个不同的偏移量。要获得总体最大值,您需要在向量中水平找到最大元素。通过改组它来做到这一点(例如,将其向右字节移动,因此高两个元素移动到低两个元素的位置),然后使用PMAXSD 将其与未打乱的值进行比较。然后重复这个过程,得到最后两个元素的最大值。

现在您可以将该 32 位 int 存储到内存中,或者使用 movd 将其作为函数返回值直接从 xmm0 传输到 eax。


这里还有一些改进的空间,因为即使pmaxsd 有一个周期的延迟(例如 Intel Haswell),它的吞吐量是每个周期 2。因此,理想情况下,我们可以通过内存操作数维持每个时钟两个 PMAX 的吞吐量。 (由于 Intel SnB 及更高版本有两个加载端口,L1 缓存可以跟上这一点。)我们需要使用多个累加器来允许并行操作。 (然后在最后将所有累加器 PMAX 在一起,然后再进行水平操作)。

;;; probably buggy, use at your own risk.  edits welcome
global max_array
max_array:
    ; function args: int *rsi, uint64_t rdi
    ; requirements: src is aligned on a 16B boundary, size is a multiple of 32bytes (8 elements), and >=8 on entry
    ; TODO: support unaligned with some startup code, and a partial final iteration with some cleanup

    lea rdx, [rsi + 4*rdi]   ; end pointer

    movdqa  xmm0, [rsi]      ; two accumulators
    movdqa  xmm1, [rsi + 16]  

    add   rsi, 32
    cmp   rsi, rdx
    jae .out        ; early exit if we shouldn't run the loop even once.  unsigned compare for addresses

.loop:
    pmaxsd  xmm0, [rsi]
    pmaxsd  xmm1, [rsi+16]
    add     rsi,  32
    cmp     rsi, rdx   ;; loop is 4 uops on Intel, since this cmp/branch macro-fuses
    jb   .loop

.out:
    ;; TODO: cleanup code to handle any non-multiple-of-8 iterations.

    pmaxsd  xmm0, xmm1
    movhlps xmm1, xmm0    ; xmm0 = { d, c, b, a}.  xmm1 = { d, c, d, c }
    pmaxsd  xmm0, xmm1    ; xmm0 = { d, c, max(d,b), max(c, a) }
                          ; if we were using AVX 3-operand instructions, we'd use PSRLDQ and another pmax because it's easy.

    ; do the final stage of horizontal MAX in integer registers, just for fun.
    ; pshufd/pmax to do the last level would be faster than this shld/cmp/cmov.

    movq    rax, xmm0     ; rax = { max(d,b), max(c,a) }
             ;  two-reg shift to unpack rax into edx:eax (with garbage in the high half of both)
    shld    rdx, rax, 32  ; rax = unchanged (eax=max(c,a)),  edx = max(d,b).
    cmp     edx, eax
    cmovg   eax, edx      ; eax = max( max(c,a), max(d,b) )
    ret

理论上,这在英特尔 SnB 系列微架构上每时钟运行一次迭代。每个时钟 4 个融合域 uops 会使管道饱和,但展开更多(并使用更多累加器)只会使非玩具版本的清理代码更令人头疼。

【讨论】:

    猜你喜欢
    • 2022-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多