我将假装这不是一个可怕的琐碎问题,并实际讨论在汇编语言中执行此操作的有趣部分(而不是让编译器为您优化它)。
在 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。请参阅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 个元素。每一个都是每 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 会使管道饱和,但展开更多(并使用更多累加器)只会使非玩具版本的清理代码更令人头疼。