给出以下函数
void foo(float* result, int size, float y, float delta) {
for (int t = 0; t < size; ++t) {
result[t] = y + delta * t;
}
}
Clang 与
-O2
生成以下 x86-64 程序集:
.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 次迭代(有 2 个 mulps
指令,每个指令覆盖 4 个 float
,并且 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;
}
}
Clang 生成的程序集要少得多 并且一次循环 16 个值。
编辑:我刚刚意识到这个版本根本没有矢量化,因此更小并且可能更慢。
编写这段代码最快的方法是什么?
正如 @user555045 指出的,这是 Clang 19 中的回归。Clang 18 及更早版本以明显的方式自动矢量化,主循环使用
cvtdq2ps
将 4 int32_t
转换为 4 float
。
如果我们查看其他 SIMD ISA 的 Clang 19 输出,例如 AArch64 和 AVX-512 (Godbolt),在这两种情况下,它都使用
unsigned
到 float
转换,例如 AArch64 ucvtf
或 AVX-512 vcvtudq2ps
。
bithack 的内容是 clang 如何将 u32 向量化为浮点型 ISA,例如 SSE2 和 AVX2,这些 ISA 只提供带符号的 int 与浮点数之间的转换。
因此,它搬起石头砸了自己的脚(对于 AVX2 及更早版本),证明
int t
始终为非负数,并将其替换为无符号临时值,忘记了它也可以按带符号工作。
这是一个您应该报告的错误 https://github.com/llvm/llvm-project/issues/。 请随意引用和/或链接到此答案。 您的 C++ 源代码是一个很好的错误报告 MCVE 测试用例。 我没有看到对
vectorization unsigned float
和类似内容的现有重复搜索。
如果
size
符号为负,则循环将运行零次迭代,并且它是 <
比较,因此它总是离开循环而不会遇到带符号溢出 UB。 无论如何,这都是 UB,所以编译器可以假设即使有 t <= size
条件或其他条件,它也不会发生。 这就是允许将 int
循环计数器提升为指针宽度的原因,以便它们可以用作数组索引,而无需每次都重新进行符号扩展。