是的,对于所有循环,SSE2 / SSE4.1 (for blendps
) / AVX / AVX-512 中的任何一个都可以实现高效的 asm,并且编译器在实践中会自动矢量化,但是 gcc7.2 / clang5.0 / ICC18 都错过了优化。
根据对 Skylake-AVX512 的静态分析(见下文),最终循环的有效展开实现可以在每 1.25 个时钟周期以一个 64 字节的结果向量运行(加上循环开销取决于展开的数量)。在实践中,如果您的数据在 L1D 缓存中很热,则每个向量可能可以实现 1.33 或 1.5 个时钟周期。否则,您很容易成为 L2 带宽的瓶颈,因为每个存储向量 64B 存储加载 2x 64B。
对于循环的 C 版本,gcc、clang 和 ICC 都或多或少地像我手动一样自动矢量化:请参阅Godbolt 编译器资源管理器上的 source + asm 。
我不得不使用-ffast-math
gcc 来自动矢量化。IDK 为什么它没有意识到它可以在不违反严格的 FP 规则的情况下安全地自动矢量化。
Clang 似乎在单独评估tmp*tmp
和tmp*tmp*tmp
混合这两个结果,而不是有条件地进行第二次乘法。
gcc 进行乘法运算并使用单独的 movaps 以另一种方式合并,因为它不知道如何反转条件。
ICC 用于KNOTW
反转条件,然后像我一样使用合并屏蔽进行第二次乘法。
更改代码以在分支而不是分支中进行额外的乘法(**3
而不是**2
),这使得所有 3 个编译器都生成了更好的代码if
else
,而它们的每一个错过的优化都不会从另一个方向分支。(仍然缺少对 gcc 的优化,但 ICC 和 clang 看起来很可靠,它们基本上都在做我手写代码所做的事情。)
ICC 选择仅使用 256b 向量对其进行自动向量化。也许它默认这样做是为了避免降低最大涡轮时钟速度?也许可以选择使用全角向量?gcc 8.0 快照也这样做,但 gcc7.2 使用 ZMM 向量。
AVX-512 掩码寄存器和合并掩码使其更加高效,但是很长一段时间以来,双向执行然后混合一直是 SIMD(甚至非 SIMD 无分支代码)的事情。例如,根据向量比较结果有条件地相加,使用该向量比较结果作为 AND 掩码,使某些元素保持不变,并使其他元素为零。
0
是加法恒等式:x + 0 = x
. x + (y&mask)
如果掩码全为零,或者掩码全为一,则无操作也是如此x+y
。请参阅如何在内部函数中使用 if 条件。(有趣的技巧:使用压缩比较结果作为整数 -1 或 0,因此您可以计算匹配但减去比较掩码)。
乘法不太简单,因为1
它是乘法恒等式,但您可以通过混合来解决这个问题。
假设编译器无论如何都没有将其优化为两个单独的循环,它可以向量化吗?
在第一种情况下,如果编译器没有将条件提升到循环之外并进行两个循环,那么您应该对编译器不满意。特别是在第二种情况下,它只需要一个循环,因为如果条件为假,则不会修改数组。
让我们谈谈第三种情况,因为它只是编译器不应该只提升条件的一种情况。(如果你的编译器感觉很笨,它可以使用这个版本和其他版本的全零或全一的循环不变掩码)。
if (c(i) > 0)
所以我们需要加载一个元素向量c
并与零进行比较。AVX512 可以对 16 个单精度向量执行此float
操作,其中一条指令具有掩码寄存器目标和内存源操作数。
; with zmm0 = 0.0 in all elements, from vxorps xmm0,xmm0,xmm0 outside the loop.
vcmpps k1, zmm0, [rdx], _CMP_NLT_UQ ; !(0 < c(i))
我知道(已经写下下一部分)对于条件为假的元素,我想要k1
为真。c(i) > 0
只有第二个向量操作数可以是内存而不是寄存器,所以我不得不反转它并使用不小于而不是不大于。(而且我不能只使用>=
而不是<
,因为这会将无序的情况(一个或两个 NaN)放在错误的类别中。FP 比较有 4 个可能的结果:高于/低于/等于/无序,所以你必须选择一个对于所有 4 种情况,您想要的谓词(即源所说的,如果您是编译器)。如果您使用 编译-ffast-math
,则允许编译器忽略 NaN 的可能性。
如果您需要将两个条件链接在一起,AVX512 compare-into-mask 指令可以使用零掩码或合并掩码来掩码写入掩码的操作。
vcmpltps k1, zmm1, zmm2 ; k1 = zmm1<zmm2
vcmpltps k2{k1}{z}, zmm3, zmm4 ; k2 = (zmm3<zmm4) & (zmm1<zmm2)
k2
在 zmm3k1 为零的任何地方都是 0,因为我们用作k1
零掩码。
if (c(i) > 0) then
a(i) = b(i) ** 2
else
a(i) = b(i) ** 3
end if
这里的常见子表达式是b(i) * b(i)
. 我们可以b(i)**3
通过乘以b(i)
一个额外的时间来得到。
vmovups zmm1, [rsi] ; load a vector from b(i)
vmulps zmm2, zmm1, zmm1 ; zmm2 = zmm1*zmm1 = b(i)**2
AVX-512 可以基于掩码作为(几乎)任何其他指令的一部分进行合并。
vmulps zmm2{k1}, zmm2, zmm1 ; zmm2 *= zmm1 for elements where k1 is true
vmovups [rdi], zmm2 ; store all 16 elements into a(i)
顺便说一句,AVX512 具有商店的合并屏蔽功能。以前的 SIMD 指令集将从 加载[rdi]
、混合,然后存储回[rdi]
. 这意味着您可以a(i)
使用每个元素条件比使用 AVX1/AVX2 更有效地实现第二个循环(有时保持不变)。
把这一切放在一起:(NASM语法)
; x86-64 System V calling convention
; args: rdi = a() output array.
; rsi = b() input array
; rdx = c() array to be tested for positive numbers
; rcx = count (in elements)
; preferably all 64-byte aligned, but will work slowly if some aren't
; rcx must be >= 16, and a multiple of 16, because I didn't write any cleanup code
global square_or_cube
square_or_cube:
vxorps xmm0, xmm0,xmm0
.loop: ; do {
vcmpps k1, zmm0, [rdx], 21 ; _CMP_NLT_UQ ; !(0 < c(i))
vmovups zmm1, [rsi] ; load a vector from b(i)
vmulps zmm2, zmm1, zmm1 ; zmm2 = zmm1*zmm1 = b(i)**2
vmulps zmm2{k1}, zmm2, zmm1 ; zmm2 *= zmm1 for elements where k1 is true, otherwise unmodified.
vmovups [rdi], zmm2 ; store all 16 elements into a(i)
; TODO: unroll some and/or use indexed addressing mode tricks to save instructions
add rdi, 64 ; pointer increments
add rsi, 64
add rdx, 64
sub rcx, 16 ; count -= 16
ja .loop ; } while(count>0);
我用 IACA 分析了这个(省略了指针增量指令来模拟展开和更聪明的 asm 技巧)。根据 IACA 的说法,即使是合并屏蔽vmulps
也是单个 uop,并且内存源指令微融合到前端的单个 uop。(商店也是。)这就是我所希望的,IACA 的输出在这种情况下看起来是正确的,尽管我无法访问 SKL-SP 硬件上的性能计数器来检查这一点。
$ iaca.sh -arch SKX avx512-conditional
Intel(R) Architecture Code Analyzer Version - 2.3 build:246dfea (Thu, 6 Jul 2017 13:38:05 +0300)
Analyzed File - avx512-conditional
Binary Format - 64Bit
Architecture - SKX
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 1.50 Cycles Throughput Bottleneck: FrontEnd
Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
---------------------------------------------------------------------------------------
| Cycles | 1.5 0.0 | 0.0 | 1.0 1.0 | 1.0 1.0 | 1.0 | 1.5 | 1.0 | 1.0 |
---------------------------------------------------------------------------------------
N - port number or number of cycles resource conflict caused delay, DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion happened
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | |
---------------------------------------------------------------------------------
| 2^ | | | 1.0 1.0 | | | 1.0 | | | CP | vcmpps k1, zmm0, zmmword ptr [rdx], 0x15
| 1 | | | | 1.0 1.0 | | | | | | vmovups zmm1, zmmword ptr [rsi]
| 1 | 1.0 | | | | | | | | CP | vmulps zmm2, zmm1, zmm1
| 1 | 0.5 | | | | | 0.5 | | | CP | vmulps zmm2{k1}, zmm2, zmm1
| 2^ | | | | | 1.0 | | | 1.0 | | vmovups zmmword ptr [rdi], zmm2
| 1 | | | | | | | 1.0 | | | sub rcx, 0x10
| 0F | | | | | | | | | | jnbe 0xffffffffffffffdd
Total Num Of Uops: 8
AVX-512 实际上有vfpclassps
(C/C++ intrinsic [_mm512_fpclass_ps_mask
] 4,asm 文档,在相关vfpclasspd
(packed double)中有一个表) 可以根据您选择的谓词对 FP 值进行分类。它可能比对另一个恰好为零的寄存器进行完全比较更有效。
(实际上,根据 IACA,它不是。InstLatx64 电子表格将两者都列为 3 个周期延迟。Agnercmpps
Fog在 Skylake-S(非 AVX512 桌面芯片)上对 AVX2 的测量显示 4 个周期,所以奇怪的是 AVX512在生成掩码寄存器结果而不是向量时,版本具有较低的延迟。
我希望结果仅对正数为假,我认为vfpclassps
可以通过设置几乎所有谓词位来获得 -Inf、有限负数、安静和信号 NaN、-0.0 和 +0.0 来做到这一点。
vfpclassps k1, [rdx], 0x1 | 0x2 | 0x4 | 0x10 | 0x40 | 0x80 ; QNaN | -0.0 | +0.0 | -Infinity | Negative (finite) | SNaN
; k1 = a 16-bit bitmap of which elements (from memory at [rdx]) need an extra multiply
vpfclassps
很有趣,因为它可以让您区分 +0.0 和 -0.0,就像您可以通过检查二进制表示中的符号位一样(就像您可以使用 AVX2vblendps
将符号位用作混合控制,而无需先进行比较)。
此外,在这种情况下,它会在循环之外保存一条指令,设置一个全零寄存器。
相关:AVX512 具有乘以2**floor(x)
( vscalefpd
) 的指令,但不能将数字提高到任意幂(整数或其他)。 Xeon Phi 有 AVX512ER,它可以为您提供快速的近似值2**x
(没有地板x
),但我们也不能在这里直接使用指数函数,而且 SKL-SP 无论如何都没有 AVX512ER。
IACA_start / end 的 NASM 宏:
我是基于iaca_marks.h
C/C++ 头文件编写的。
%if 1
%macro IACA_start 0
mov ebx, 111
db 0x64, 0x67, 0x90
%endmacro
%macro IACA_end 0
mov ebx, 222
db 0x64, 0x67, 0x90
%endmacro
%else
%define IACA_start
%define IACA_end
%endif
将它们包裹在您要分析的任何代码周围。
循环内循环不变条件的条件分支
编译器可以在循环内分支。IDK 如果有的话会编写这样的代码,但他们当然可以。
; rdi = destination
; rsi = source
; edx = condition
; rcx = element count
global square_or_cube
square_or_cube:
.loop: ; do {
vmovups zmm1, [rsi] ; load a vector from b(i)
vmulps zmm2, zmm1, zmm1 ; zmm2 = zmm1*zmm1 = b(i)**2
test edx,edx
jz .only_square ; test-and-branch to conditionally skip the 2nd multiply
vmulps zmm2, zmm2, zmm1 ; zmm2 *= zmm1
.only_square:
vmovups [rdi], zmm2 ; store all 16 elements into a(i)
add rdi, 64 ; pointer increments
add rsi, 64
sub rcx, 16 ; count -= 16
ja .loop ; } while(count>0);