给定以下函数

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

  • 3
    这种奇怪的东西似乎是我见过的最奇怪的将 int 转换为 float 的方法。CVTDQ2PS存在,我不知道为什么 Clang 会合成一些奇怪的技巧。转换无符号 int 需要一些技巧,但t有符号。E:这是 Clang 19 中的回归,在此之前


    – 


  • 1
    .L4在我看来,GCC 输出很正常。主要内容是和 分支之间的代码.L4,其中没有任何异常。


    – 

  • 1
    顺便说一下,在第二个示例中,Clang 没有对循环进行矢量化,因此您有一个相对高延迟的循环,其中包含依赖项。代码较少,但速度可能也较慢。


    – 

  • 1
    第二个版本需要-ffast-math自动矢量化,因为您有一系列需要以不同顺序执行的FP加法。(或者可能是 OpenMP,但我不确定是否#pragma omp simd reduction(y:+)正确;也许除了归纳变量的减少之外还有其他方法?)


    – 

  • 1
    正如 @user555045 指出的那样,FP-add 延迟是第二个版本的瓶颈。即使您只使用一个y向量对其进行矢量化,情况也是如此,就像 clang 所做的那样-O2 -ffast-math)。请参阅回复:展开,clang 自动矢量化的代码可以饱和我的 Skylake 的 2x 256 位 FP 加法 + 1x 每时钟存储。(这是一个二阶多项式,而您的只是一阶多项式,因此每个存储加 1 次。)


    – 


最佳答案
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,可能还有一个指针增量)会消耗吞吐量,并且不会让无序执行领先。