给定以下函数
void foo(float* result, int size, float y, float delta) {
for (int t = 0; t < size; ++t) {
result[t] = y + delta * t;
}
}
Clang-O2
:
.LCPI0_0:
.long 0
.long 1
.long 2
.long 3
.LCPI0_1:
.long 4
.long 4
.long 4
.long 4
.LCPI0_2:
.long 65535
.long 65535
.long 65535
.long 65535
.LCPI0_3:
.long 1258291200
.long 1258291200
.long 1258291200
.long 1258291200
.LCPI0_4:
.long 1392508928
.long 1392508928
.long 1392508928
.long 1392508928
.LCPI0_5:
.long 0x53000080
.long 0x53000080
.long 0x53000080
.long 0x53000080
.LCPI0_6:
.long 8
.long 8
.long 8
.long 8
foo(float*, int, float, float):
test esi, esi
jle .LBB0_7
mov eax, esi
cmp esi, 7
ja .LBB0_3
xor ecx, ecx
jmp .LBB0_6
.LBB0_3:
mov ecx, eax
and ecx, 2147483640
movaps xmm2, xmm1
shufps xmm2, xmm1, 0
movaps xmm3, xmm0
shufps xmm3, xmm0, 0
mov edx, eax
shr edx, 3
and edx, 268435455
shl rdx, 5
movdqa xmm4, xmmword ptr [rip + .LCPI0_0]
xor esi, esi
movdqa xmm5, xmmword ptr [rip + .LCPI0_1]
movdqa xmm6, xmmword ptr [rip + .LCPI0_2]
movdqa xmm7, xmmword ptr [rip + .LCPI0_3]
movdqa xmm8, xmmword ptr [rip + .LCPI0_4]
movaps xmm9, xmmword ptr [rip + .LCPI0_5]
movdqa xmm10, xmmword ptr [rip + .LCPI0_6]
.LBB0_4:
movdqa xmm11, xmm4
paddd xmm11, xmm5
movdqa xmm12, xmm4
pand xmm12, xmm6
por xmm12, xmm7
movdqa xmm13, xmm4
psrld xmm13, 16
por xmm13, xmm8
subps xmm13, xmm9
addps xmm13, xmm12
movdqa xmm12, xmm11
pand xmm12, xmm6
por xmm12, xmm7
psrld xmm11, 16
por xmm11, xmm8
subps xmm11, xmm9
addps xmm11, xmm12
mulps xmm13, xmm2
addps xmm13, xmm3
mulps xmm11, xmm2
addps xmm11, xmm3
movups xmmword ptr [rdi + rsi], xmm13
movups xmmword ptr [rdi + rsi + 16], xmm11
paddd xmm4, xmm10
add rsi, 32
cmp rdx, rsi
jne .LBB0_4
cmp ecx, eax
je .LBB0_7
.LBB0_6:
xorps xmm2, xmm2
cvtsi2ss xmm2, ecx
mulss xmm2, xmm1
addss xmm2, xmm0
movss dword ptr [rdi + 4*rcx], xmm2
inc rcx
cmp rax, rcx
jne .LBB0_6
.LBB0_7:
ret
我试图了解这里发生了什么。这似乎.LBB0_4
是一个循环,每次迭代都覆盖原始循环的 8 次迭代(有 2mulps
条指令,每条指令覆盖 4float
次并rsi
增加 32)。最后的代码可能是为了解决size
不能被 8 整除的情况。我遇到麻烦的是其余的代码。循环内的所有其他指令.LBB0_4
和开头的常量在做什么?是否有工具或编译器参数可以帮助我理解 SIMD 矢量化的结果?也许可以使用 SIMD 内在函数将其转换回 C++?
如果我将代码改为这样
void foo(float* result, int size, float y, float delta) {
for (int t = 0; t < size; ++t) {
result[t] = y;
y += delta;
}
}
。
编辑:我刚刚意识到这个版本根本没有矢量化,因此更小而且可能更慢。
编写此代码的最快方法是什么?
12
最佳答案
1
正如@user555045 指出的那样,这是 Clang 19 中的回归。Clang 18 和更早版本以明显的方式自动矢量化,主循环用于cvtdq2ps
将 4 转换int32_t
为 4 float
。
如果我们查看其他 SIMD ISA(如 AArch64 和 AVX-512())的 Clang 19 输出,在两种情况下它都使用unsigned
转换float
,如 AArch64ucvtf
或 AVX-512 vcvtudq2ps
。
bithack 的内容是 clang 如何将 u32 矢量化为浮点数,以适应 SSE2 和 AVX2 等 ISA,它们仅提供有符号整数到浮点数的转换。
因此,它自食其果(对于 AVX2 及更早版本而言),证明int t
始终是非负的,并将其替换为无符号临时数,而忘记了它也可以作为有符号数工作。
这是一个您应该在 请随意引用和/或链接到此答案。您的 C++ 源代码是一个很好的 MCVE 错误报告测试用例。我没有看到现有的重复搜索vectorization unsigned float
和类似的东西。
如果符号为负,则循环将运行零次迭代size
,并且它是一个<
比较,因此它总是在不遇到有符号溢出 UB 的情况下离开循环。无论如何,这都是 UB,因此编译器可以假设它不会发生,即使有条件t <= size
或其他情况也是如此。这就是允许将int
循环计数器提升到指针宽度的原因,这样它们就可以用作数组索引,而无需每次都重新进行符号扩展。
编写此代码的最快方法是什么?
请参阅 – 避免每次迭代都进行转换和乘法(或 FMA)可以取得胜利,但前提是您展开得足够多以使用多个向量累加器隐藏 FP 延迟。像float y[UNROLL] = ...;
y[0] += UNROLL * delta;
y[1] += UNROLL * delta;
……等等。
如果展开足够多,编译器就会使用 6、8 或 10 个向量作为 元素y[]
,这勉强够用vaddps
现代 x86 上的 延迟 x 吞吐量乘积,通常为 3 到 4 个周期延迟,2/时钟吞吐量,因此一次可执行 6 到 8 个。一些冗余可实现不完美的调度。
在 Ice Lake 或更高版本的 Intel 上,以及可能是 Zen 3 或更高版本的 AMD 上,这可以实现vaddps
每时钟 2x 256 位和 2x 256 位矢量存储。 (Skylake 及更早版本每时钟只能进行一次存储。 。链接的问答是关于二阶多项式的,因此使用差分法每存储需要 2 次加法。因此,我的 Skylake 能够使用我的代码每时钟进行 2 次加法和 1 次存储。Ice Lake 会有多余的存储带宽。当然,假设目标在 L1d 缓存中很热,否则即使对于 L2,您也很可能遇到内存/缓存带宽瓶颈。)
在 Intel E 核(128 位宽 SIMD)上,添加与存储的吞吐量比也是 1:1,因此您再次需要仅执行 SIMD FP 添加的展开版本。
整数加法、转换和 FMA 每时钟仅 3 个 SIMD 矢量 ALU 微指令,因此实际上可以以每时钟 1 个矢量结果运行,这足以跟上 Skylake 上的存储吞吐量,而无需降低强度到仅添加。(-march=x86-64-v3
当然,如果您使用编译,则允许 FMA)。但是前端是一个瓶颈,因为它在 SKL 及更早版本上只有 4 个微指令宽。因此循环开销(至少一个宏融合的添加或 sub/jcc,可能还有一个指针增量)会消耗吞吐量,并且不会让无序执行领先。
|
CVTDQ2PS
存在,我不知道为什么 Clang 会合成一些奇怪的技巧。转换无符号 int 需要一些技巧,但t
有符号。E:这是 Clang 19 中的回归,在此之前–
.L4
在我看来,GCC 输出很正常。主要内容是和 分支之间的代码.L4
,其中没有任何异常。–
–
-ffast-math
自动矢量化,因为您有一系列需要以不同顺序执行的FP加法。(或者可能是 OpenMP,但我不确定是否#pragma omp simd reduction(y:+)
正确;也许除了归纳变量的减少之外还有其他方法?)–
y
向量对其进行矢量化,情况也是如此,就像 clang 所做的那样-O2 -ffast-math
()。请参阅回复:展开,clang 自动矢量化的代码可以饱和我的 Skylake 的 2x 256 位 FP 加法 + 1x 每时钟存储。(这是一个二阶多项式,而您的只是一阶多项式,因此每个存储加 1 次。)–
|