如何比较汇编语言x86中数组中的所有元素?我必须比较数组中的所有元素并打印最大的元素
答案 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元素的最大值。请参阅x86 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个元素。对于四种不同的偏移,每一个都是每个第四个元素的最大值。要获得最大值,您需要在向量中水平查找最大元素。通过对其进行混洗来实现这一点(例如,将其向右移位,使高两个元素移动到低两个元素的位置),然后使用PMAXSD
将其与未混洗的值进行比较。然后重复该过程,以获得最后两个元素的最大值。
现在您可以将32位int存储到内存中,或使用movd
将xmm0
直接从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使管道饱和,但是展开更多(并使用更多累加器)只会使非玩具版本的清理代码更加令人头疼。