了解 Clang 的 SIMD 优化,用于将 float 乘以 int 循环计数器

问题描述 投票:0回答:1

给出以下函数

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 个值

编辑:我刚刚意识到这个版本根本没有矢量化,因此更小并且可能更慢。

编写这段代码最快的方法是什么?

c++ assembly clang x86-64 simd
1个回答
0
投票

正如 @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
循环计数器提升为指针宽度的原因,以便它们可以用作数组索引,而无需每次都重新进行符号扩展。

© www.soinside.com 2019 - 2024. All rights reserved.