如何在程序集中比较数组中的元素

时间:2015-12-01 10:49:28

标签: assembly

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

1 个答案:

答案 0 :(得分:9)

我会假装这不是一个可怕的微不足道的问题,并且实际上讨论了用汇编语言执行此操作的有趣部分(而不是让编译器为您优化它)。

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

由于在asm中编写代码的主要原因是high performance,所以让我们考虑一些问题:

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

在找到最大值时,分支误预测可能不常见,除非您的值有噪音但平均增加。 (例如,每1到10个元素会出现一个新的最大值,接近最坏情况。)否则,您可能会长时间看到新的最大值或从未看到新的最大值。

x86具有向量最小/最大指令,其作用类似于每个元素的cmp / cmov。

因此,如果你的数组由32位有符号整数组成,你可以使用start将前4个元素加载到向量寄存器(比如xmm0),然后在循环中使用add rsi, 16 / PMAXSD xmm0, [rsi]来做4次打包x=max(x,src)次操作。英语中的PMAXSD是:Packed(整数)有符号DWord元素的最大值。请参阅 wiki中的链接以获取参考指南。 PMAXSD是SSE4.1的一部分,因此仅在具有该功能位的CPU上支持。

如果您的数组由uint8_t元素组成,则您使用PMINUB(Packed(int)Min of Unsigned Byte元素)。 PMIN/MAXUBPMIN/MAXSW位于SSE2中,因此它们是x86-64的基线(对于需要足够新硬件且支持SSE2的操作系统上的x86-32)。

在循环数组之后(可能使用PALIGNR或PSRLDQ来处理数组的最后一个非16B位),你的累加器向量中有4个元素。对于四种不同的偏移,每一个都是每个第四个元素的最大值。要获得最大值,您需要在向量中水平查找最大元素。通过对其进行混洗来实现这一点(例如,将其向右移位,使高两个元素移动到低两个元素的位置),然后使用PMAXSD将其与未混洗的值进行比较。然后重复该过程,以获得最后两个元素的最大值。

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

这里还有一些改进空间,因为即使;;; 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 具有一个周期的延迟(例如Intel Haswell),它的每个周期的吞吐量为2。理想情况下,我们可以通过内存操作数维持每个时钟两个PMAX的吞吐量。 (由于Intel SnB及以后有两个加载端口,L1缓存可以跟上这一点。)我们需要使用多个累加器来允许并行操作。 (然后在完成水平操作之前将PMAX所有累加器放在一起)。

{{1}}

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