如果您关心性能,请避免loop
使用现代 CPU 上的指令。 为什么循环指令很慢?英特尔不能有效地实施它吗?. 也使用 SSE2 代替 MMX;您的数组大小是 16 和 8 的倍数,并且您使用的是 x86-64,它保证具有 SSE2。MMX 对此毫无意义,除非您还为 Pentium III / Athlon-XP 和更早版本制作 32 位版本。
(我的答案中的所有代码都将使用 8 字节 MMX 寄存器而不是 16 字节 XMM 寄存器,因为我使用的所有指令都有 MMX 版本。根据 NASM 手册的附录B,所有这些都在原始 P5 Pentium MMX 中可用。英特尔手册中列为“MMX”的一些指令(如和)只是后来才添加的,比如可能与 Pentium II 一起使用,或者与 Pentium III 中的 SSE 一起添加,但任何情况都不是这样此处有用的说明。)pmullw
pxor
pcmpgtw
paddusw
pmulhuw
pshufw
请参阅https://stackoverflow.com/tags/x86/info以获取性能/优化指南,以及解释调用函数所需的 16 字节堆栈对齐的 ABI/调用约定链接。
mov rax, 0
/mov ax, [r8]
真的很傻。movzx eax, word [r8]
像正常人一样使用。您也不需要到jmp
下一个源代码行,例如jmp .square_loop
/ .square_loop:
Execution 如果没有分支指令,总是会自行落到下一行。
x86 SIMD 没有饱和乘法,只有饱和有符号/无符号加法和饱和打包到更窄的元素。(MMX/SSE2 paddsw
/ paddusw
)。既然你用 打印%d
,也许你想要签名饱和度?但这只是在解压缩为 32 位之后,您的公式将始终产生积极的结果,因此您可以使用无符号饱和度。我看到这就是您的代码正在使用的内容paddusw
。
此外,在公式的每个步骤之间使用 3 个单独的循环来存储/重新加载您的数据真的很糟糕。您(几乎)总是希望增加计算强度(每个内存/缓存带宽对数据完成的 ALU 工作量)。您也不需要乘法指令来将数字加倍:只需将其添加到自身。 padd*
在比 更多的端口上运行pmul*
,并且具有更好的延迟和(在这种情况下相关)吞吐量。
default rel
;;; Efficient but *doesn't* saturate the multiply
lea rcx, [rsi + length] ; rcx = end_pointer
movdqa xmm5, [fives]
.loop: ; do{
movdqu xmm0, [rsi] ; use movdqa if your data is aligned, faster on very old CPUs.
pmullw xmm0, xmm0 ; x*x ; does NOT saturate. will wrap.
paddusw xmm0, xmm0 ; *= 2 ; with unsigned saturation
paddusw xmm0, xmm5 ; += 5 ; xmm5 = _mm_set1_epi16(5) outside the loop
movdqu [rsi], xmm0
add rsi, 16
cmp rsi, rcx ; }while(p<endp);
jb .loop
...
section .rodata
align 16
fives: times 8 dw 5
对于饱和,您可能可以使用 SSSE3 https://www.felixcloutier.com/x86/pmaddubsw,但这只需要字节输入。它使 i8 x u8 => i16 乘积对的水平总和饱和。
否则,您可能必须解压缩为双字并packssdw
(有符号)或packusdw
(无符号饱和)返回单词。但是 SSE4.1 的双字乘法速度很慢pmulld
(Haswell 及更高版本为 2 微秒)。不过,在一些较旧的 CPU 上实际上只有 1 uop。当然,拆包会因拥有更广泛的元素而产生两倍的工作量。
在这种情况下,您的公式与输入的大小是单调的,因此您可以只比较输入并手动饱和。
如果我们假设您的输入也是无符号的,我们不必在比较之前做一个绝对值。但是(直到 AVX512)我们没有无符号整数比较,只有有符号大于,所以大的无符号输入将比较为负。
2 * 0x00b5^2 + 5 = 0xfff7
适合 16 位
2 * 0x00b6^2 + 5 = 0x102cd
没有,我们希望它饱和到0xffff
溢出截止点是偶数,因此我们可以通过右移来处理有符号比较问题。那将是无符号除以 2,让 use 安全地将结果视为非负有符号整数。 0xb6 >> 1 = 0x5b
. 但是pcmpgtw
是比较>
,不是>=
。
如果我们将操作数反转pcmpgtw
为比较 for (x>>1) < (0xb6>>1)
,那么我们必须movdqa
使用常量以避免破坏它,但我们仍然需要x
使用 movdqa+psrlw 进行右移。并且在需要饱和时(否则为 0)拥有一个向量会更有效0xffff
,因为我们可以使用 POR 或 PADDUSW 直接应用它。
所以我们最好的选择是简单地对 x 和0xb5
有符号进行范围移位,并(x-0x8000) > (0xb5 - 0x8000)
使用pcmpgtw
SIMD 有符号比较。
其他更糟糕的选择包括:
- 检查乘以中的溢出
pmulhuw
以计算高半部分(并检查它是否非零)。我们将面临乘数吞吐量瓶颈的危险,并且检查非零与pcmpeqw
我们想要的条件相反。
psubusw x, 0xb5
并检查它是否为 == 0. pcmpeqw
会给我们一个反转掩码,但我们不能用于pcmpgtw
检查,usat16(x-0xb5) > 0
因为大输入(设置了高位)在仅减去一个小数(如0xb5
.
paddusw
并检查== 0xffff
:只有足够小的输入才会使最终结果不饱和。一些 CPU 可以pxor
在比 更多的端口上运行padd*
,并且它不需要任何更少的非零向量常数,所以这无论如何都不是更好。但它在 Skylake 上同样出色。
default rel
;;; With a check on the input to saturate the output
lea rcx, [rsi + length] ; rcx = end_pointer
movdqa xmm4, [input_saturation_cutoff]
movdqa xmm5, [fives]
pcmpeqd xmm3, xmm3
psllw xmm3, 15 ; xmm3 = set1_epi16(0x8000) for range-shifting to signed
.loop:
movdqu xmm0, [rsi]
movdqa xmm1, xmm0
; if x>0xb5 (unsigned), saturate output to 0xffff
pxor xmm1, xmm3 ; x - 0x8000 = range shift to signed for compare
pcmpgtw xmm1, xmm4 ; xmm1 = (x > 0xb5) ? -1 : 0
pmullw xmm0, xmm0 ; x*x
paddusw xmm0, xmm0 ; *= 2 ; saturating add or not doesn't matter here
por xmm1, xmm5 ; 0xffff (saturation needed) else 5. Off the critical path to give OoO exec an easier time.
paddusw xmm0, xmm1 ; += 5 or += 0xffff with unsigned saturation.
movdqu [rsi], xmm0
add rsi, 16
cmp rsi, rcx
jb .loop
...
section .rodata
align 16
input_saturation_cutoff: times 8 dw (0x00b5 - 0x8000) ; range-shifted to signed for pcmpgtw
fives: times 8 dw 5
; 5 = 0xb6 >> 5 or 0xb6 >> 5 but we'd need to knock off the high bit from input_saturation_cutoff
; Or we could materialize constants like this:
; movdqa xmm4, [set1w_0xb5]
; pcmpeqd xmm3, xmm3
; psllw xmm3, 15 ; rangeshift_mask = set1(0x8000)
; movdqa xmm5, xmm4
; psrlw xmm5, 5 ; fives = set1(5)
; pxor xmm4, xmm3 ; input_sat_cutoff = set1(0x80b5)
;; probably not worth it since we already need to load 1 from memory.
我对此进行了测试,并且paddusw
确实这样做0x2 + 0xffff = 0xffff
了。
我们可以使用 0 或 0xffff 对最终结果进行 POR 以使其保持不变或将其设置为 0xffff,但将输入修改为最后一个paddusw
会在一次迭代中创建更多指令级并行性。所以乱序执行不必重叠尽可能多的独立迭代来隐藏循环体的延迟。(如果我们实际上是为有序的 Atom 或 P5 Pentium-MMX 安排这个,我们会交错更多的两个 dep 链。)
实际上,右移 1是可行的:我们只需要比较来捕获如此大的输入,以至于乘法换行到一个小的结果。 0xb6 * 0xb6
不换行,所以它自己饱和就好了paddubsw
。
如果我们检查(x>>1) > (0xb6>>1)
(pcmpgtw
而不是>=
)来捕获像0xffff
(其中 pmullw 和 0xffff 给我们 0x0001)这样的输入,那很好。所以我们可以保存 1 个向量常数,否则这不是更好。
pxor
+pcmpgtw
至少和psrlw xmm, 1
+一样便宜pcmpgtw
,除非我们正在调整 Intel P6 系列(Core2/Nehalem)并遇到寄存器读取端口停顿。但这不太可能:xmm0、xmm1 和 rsi 应该始终是热的(最近写入并因此从 ROB 中读取,而不是从永久寄存器文件中读取)。我们只在循环中的第一组 4 条指令中读取 2 个向量常量,然后再读取 1 个。
正如我在下面所说,在许多 Intel CPU 上,psrlw
只能pmullw
在 vec-int shift+multiply 执行单元上运行在与 vec-int shift+multiply 相同的端口上。不过,这里可能不是吞吐量瓶颈。
但是pcmp
并padd
在有限的端口上运行(在 Skylake 之前的 Intel 上),同时pxor
可以在任何矢量 ALU 端口上运行。 纯padd
/ pcmp
/ pmul
/psrl` uops 的混合将留下一个向量 ALU 端口未使用。
交替饱和度检查的想法
(当我寻找不会溢出的最高输入时,我写这部分时忘记了公式中的 *2。)
如果公式是(0x00ff)^2 + 5
,饱和度检查会更简单。
我们可以只检查位位置。
(0x00ff)^2 + 5 = 0xfe06
适合 16 位
(0x0100)^2 + 5 = 0x10005
没有,我们希望它饱和到0xffff
所以我们需要检查高 16 位是否全为零,或者 that x&0xFF == x
,或者 that x>>8 == 0
。
这需要更少的常量,但实际上比使用 PXOR 进行有符号的范围移位更糟糕,因为在某些 CPU 上,向量移位和向量乘法执行单元位于同一端口上。(因此psrlw
并pmullw
相互竞争吞吐量。这是足够的总微指令,我们实际上不会在 Nehalem / Sandybridge / Haswell 的端口 0 上出现瓶颈,但它不会受到伤害。)
lea rcx, [rsi + length] ; rcx = end_pointer
movq xmm5, [fives]
punpcklqdq xmm5, xmm5 ; or with SSE3, movddup xmm5, [fives] to broadcast-load
pxor xmm4, xmm4 ; xmm4 = 0
.loop:
movdqu xmm0, [rsi]
movdqa xmm1, xmm0
; if x>0xffU, i.e. if the high byte >0, saturate output to 0xffff
psrlw xmm1, 8 ; x>>8 (logical right shift)
pcmpgtw xmm1, xmm4 ; xmm1 = ((x>>8) > 0) ? -1 : 0
pmullw xmm0, xmm0 ; x*x ; does NOT saturate. will wrap.
paddusw xmm0, xmm0 ; *= 2 ; with unsigned saturation
por xmm1, xmm5 ; 0xffff (saturation needed) or 5 (normal). Off the critical path to give OoO exec an easier time.
paddusw xmm0, xmm1 ; += 5 or += 0xffff with unsigned saturation.
movdqu [rsi], xmm0
add rsi, 16
cmp rsi, rcx
jb .loop
使用 AVX512BW (Skylake-X) 进行无符号比较,使用掩码寄存器
我们终于可以使用 AVX512F 进行无符号整数比较,并使用 AVX512BW 对字元大小进行比较。但是结果是在一个掩码寄存器中而不是一个向量中,所以我们不能只vpor
用它的向量set1(5)
来创建一个用于饱和加法的输入。
相反,我们可以根据比较掩码在5
和的向量之间进行混合。0xffff
;; AVX512BW version
;; On a Skylake Xeon you probably only want to use YMM regs unless you spend a lot of time in this
;; to avoid reducing max turbo much.
;; Even with xmm or ymm regs (AVX512VL + BW), this demonstrates
;; that we gain even more efficiency than just widening the vectors
;; Just having non-destructive AVX encodings saves the `movdqa xmm1,xmm0` in the SSE2 version.
;; With YMM or XMM regs, most of these instructions can still use shorter VEX encoding (AVX), not the longer EVEX (AVX512)
;; (Use vmovdqa/u instead of vmovdqu64. The 64 is element-size, irrelevant when not masking.)
;;;;;;;;;;; UNTESTED ;;;;;;;;;;;;;;;;;
mov eax, 0xb5 ;; materialize vector constants from an immediate
vpbroadcastd zmm4, eax ; largest input that won't overflow/saturate
vpsrlw zmm5, zmm4, 5 ; fives = 0xb5 >> 5 = 5
;vpcmpeqd xmm3, xmm3 ; all-ones: result on saturation
vpternlogd zmm3,zmm3,zmm3, 0xff ; alternative for ZMM regs, where there's no compare-into-vector
.loop:
; alignment recommended for 512-bit vectors, but `u` lets it still work (slower) on unaligned.
vmovdqu64 zmm0, [rsi]
;; if x>0xb5 (unsigned), saturate output to 0xffff
vpcmpuw k1, zmm0, zmm4, 2 ; k1 = x <= 0xb5. 2 = LE predicate
; k1 set for elements that WON'T overflow
vpmullw zmm0, zmm0 ; x*x
vpaddusw zmm0, zmm0 ; *= 2 ; saturating add or not doesn't matter here
vmovdqa64 zmm1, zmm3 ; 0xffff
vpaddusw zmm1{k1}, zmm0, zmm5 ; 0xffff or 2*x^2 + 5 merge masking
vmovdqu64 [rsi], zmm1
add rsi, 64
cmp rsi, rcx
jb .loop
(当您不想利用非破坏性目标 3 操作数编码时,NASM 允许vpmullw a, b
作为 的快捷方式,就像它为 . 所做的那样。)vpaddusw a, a, b
imul eax, 123
一个较早的应用饱和度的想法是用于根据掩码vpblendmw
在向量之间进行选择5
。0xffff
vpcmpuw k1, xmm4, xmm0, 1 ; k1 = 0xb5<x = x>0xb5. 1 = LT predicate numerically because NASM doesn't seem to accept vpcmpltuw the way it accepts vpcmpeqb
; k1 = 1 for elements that WILL overflow.
multiply and add as usual
...
vpblendmw xmm1{k1}, xmm5, xmm3 ; select (k1==0) ? 5 : 0xffff
vpaddusw xmm0, xmm1 ; += 5 or += 0xffff with unsigned saturation.
复制寄存器仍然需要前端微指令,但不需要后端 ALU 微指令。所以(特别是对于 512 位寄存器,其中端口 1 为 SKX 上的向量微指令关闭),这个vpblendmb
想法比复制和合并掩码更糟糕。
除此之外,IACA 认为vpblendmw xmm1{k1}, xmm3, xmm5
对 XMM1 有输出依赖性,即使它实际上是 write only。(我通过将其中的 8 个放在一个循环中进行测试,有/没有 dep-break vpxor
)。掩码混合指令是一种特殊情况:对于未设置掩码位意味着它从 src1 复制(或零掩码为零),而对于设置掩码位它从 src2 复制。
但是机器编码使用合并掩码,因此硬件可能会将其视为具有合并掩码的任何其他 ALU 操作。(其中输出向量是第三个输入依赖vpaddw xmm1{k1}, xmm2, xmm3
项:如果掩码有任何零,则 XMM1 中的结果将是该元素的输入值。)
这可能不是问题:根据 IACA,SKX 可以每 2.24 个周期运行一次迭代(在前端有瓶颈),因此通过 XMM1 的循环承载的 dep 链只要它只有 1 个就不是问题周期延迟。(如果展开以减少循环开销/前端瓶颈,您应该为混合目标使用单独的向量来解耦迭代,即使您无法在每次迭代接近 1 个周期的任何地方将其降低。)
(并且使用复制 + 合并掩码到向量的版本0xffff
也以该吞吐量运行,即使对于 ZMM 向量也是如此。但 IACA 认为使用 ZMM 的 vpblendmb 版本会更慢,即使它说前端都存在瓶颈。 .)