当 -mavx2 开启时,为 __builtin_popcnt 生成了臃肿的代码

Bloated code generated for __builtin_popcnt when -mavx2 is on

对于这样的函数,clang(有时 gcc 在某些我无法最低限度重现的上下文中)似乎在 -mavx2 开关打开时生成臃肿的代码。

unsigned count(uint64_t *f) {
    unsigned c = 0;
    for (unsigned i = 0; i < 1024; ++i) {
        if (sizeof(long) >= 8) {
            c += __builtin_popcountl(f[i]);
        } else {
            c += __builtin_popcountll(f[i]);
        }
    }
    return c;
}

这来自 gcc,非常简单。

count:
        lea     rcx, [rdi+8192]
        xor     eax, eax
.L2:
        xor     edx, edx
        add     rdi, 8
        popcnt  rdx, QWORD PTR [rdi-8]
        add     eax, edx
        cmp     rcx, rdi
        jne     .L2
        ret

但是 clang 决定在 -mavx2 开启时产生这种巨大的膨胀。 -mpopcnt 也已设置。

.LCPI0_0:
        .zero   32,15
.LCPI0_1:
        .byte   0                               # 0x0
        .byte   1                               # 0x1
        .byte   1                               # 0x1
        .byte   2                               # 0x2
        .byte   1                               # 0x1
        .byte   2                               # 0x2
        .byte   2                               # 0x2
        .byte   3                               # 0x3
        .byte   1                               # 0x1
        .byte   2                               # 0x2
        .byte   2                               # 0x2
        .byte   3                               # 0x3
        .byte   2                               # 0x2
        .byte   3                               # 0x3
        .byte   3                               # 0x3
        .byte   4                               # 0x4
        .byte   0                               # 0x0
        .byte   1                               # 0x1
        .byte   1                               # 0x1
        .byte   2                               # 0x2
        .byte   1                               # 0x1
        .byte   2                               # 0x2
        .byte   2                               # 0x2
        .byte   3                               # 0x3
        .byte   1                               # 0x1
        .byte   2                               # 0x2
        .byte   2                               # 0x2
        .byte   3                               # 0x3
        .byte   2                               # 0x2
        .byte   3                               # 0x3
        .byte   3                               # 0x3
        .byte   4                               # 0x4
count:                                  # @count
        vpxor   xmm0, xmm0, xmm0
        xor     eax, eax
        vmovdqa ymm1, ymmword ptr [rip + .LCPI0_0] # ymm1 = [15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15]
        vmovdqa ymm2, ymmword ptr [rip + .LCPI0_1] # ymm2 = [0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4]
        vpxor   xmm12, xmm12, xmm12
        vpxor   xmm4, xmm4, xmm4
        vpxor   xmm5, xmm5, xmm5
        vpxor   xmm6, xmm6, xmm6
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vmovdqu ymm7, ymmword ptr [rdi + 8*rax]
        vmovdqu ymm8, ymmword ptr [rdi + 8*rax + 32]
        vmovdqu ymm9, ymmword ptr [rdi + 8*rax + 64]
        vmovdqu ymm10, ymmword ptr [rdi + 8*rax + 96]
        vpand   ymm11, ymm7, ymm1
        vpshufb ymm11, ymm2, ymm11
        vpsrlw  ymm7, ymm7, 4
        vpand   ymm7, ymm7, ymm1
        vpshufb ymm7, ymm2, ymm7
        vpaddb  ymm7, ymm11, ymm7
        vpsadbw ymm7, ymm12, ymm7
        vpand   ymm11, ymm8, ymm1
        vpshufb ymm11, ymm2, ymm11
        vpsrlw  ymm8, ymm8, 4
        vpand   ymm8, ymm8, ymm1
        vpshufb ymm8, ymm2, ymm8
        vpaddb  ymm8, ymm8, ymm11
        vpsadbw ymm8, ymm8, ymm12
        vpand   ymm11, ymm9, ymm1
        vpshufb ymm11, ymm2, ymm11
        vpsrlw  ymm9, ymm9, 4
        vpand   ymm9, ymm9, ymm1
        vpshufb ymm9, ymm2, ymm9
        vpaddb  ymm9, ymm9, ymm11
        vpsadbw ymm9, ymm9, ymm12
        vpand   ymm11, ymm10, ymm1
        vpshufb ymm11, ymm2, ymm11
        vpsrlw  ymm10, ymm10, 4
        vpand   ymm10, ymm10, ymm1
        vpshufb ymm10, ymm2, ymm10
        vpaddb  ymm10, ymm10, ymm11
        vpsadbw ymm10, ymm10, ymm12
        vextracti128    xmm3, ymm7, 1
        vpackusdw       xmm3, xmm7, xmm3
        vpaddd  xmm0, xmm0, xmm3
        vextracti128    xmm3, ymm8, 1
        vpackusdw       xmm3, xmm8, xmm3
        vpaddd  xmm4, xmm4, xmm3
        vextracti128    xmm3, ymm9, 1
        vpackusdw       xmm3, xmm9, xmm3
        vpaddd  xmm5, xmm5, xmm3
        vextracti128    xmm3, ymm10, 1
        vpackusdw       xmm3, xmm10, xmm3
        vpaddd  xmm6, xmm6, xmm3
        add     rax, 16
        cmp     rax, 1024
        jne     .LBB0_1
        vpaddd  xmm0, xmm4, xmm0
        vpaddd  xmm0, xmm5, xmm0
        vpaddd  xmm0, xmm6, xmm0
        vpshufd xmm1, xmm0, 238                 # xmm1 = xmm0[2,3,2,3]
        vpaddd  xmm0, xmm0, xmm1
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        vzeroupper
        ret

clang 的代码类似于 gcc 时只有 -mpopcnt 打开,有一点展开。

count:                                  # @count
        xor     ecx, ecx
        xor     eax, eax
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        popcnt  rdx, qword ptr [rdi + 8*rcx]
        add     edx, eax
        popcnt  rsi, qword ptr [rdi + 8*rcx + 8]
        add     esi, edx
        popcnt  rdx, qword ptr [rdi + 8*rcx + 16]
        popcnt  rax, qword ptr [rdi + 8*rcx + 24]
        add     edx, esi
        add     eax, edx
        add     rcx, 4
        cmp     rcx, 1024
        jne     .LBB0_1
        ret

根据此文档 (https://www.agner.org/optimize/instruction_tables.pdf),popcnt 在大多数体系结构上都是非常便宜的指令。那为什么 clang 生成这样的膨胀来替换 popcnt 而我明确允许它与 -mpopcnt 一起使用?优化级别全部设为-O3.

这里有一个 link 神栓 (https://godbolt.org/z/4vWK33a7c).

它是自动矢量化和展开的,这对于大型阵列来说是一个性能上的胜利(或者如果 clang 的开销更少的话),至少在 Intel CPU 上是这样 popcnt 是 1/时钟,所以每个时钟 64 位。 (AMD Zen 有 3 或 4 个/clock popcnt,因此 add 指令占用 4 个标量整数 ALU 端口,它可以维持 2/clock uint64_t popcnt+load并添加。) https://uops.info/

但是 vpshufb 在 Intel 上也是 1/clock(或者在 Ice Lake 上是 2/clock),如果瓶颈是每个时钟 128 位的 popcount 工作。 (对 32 个字节中每个字节的低 4 位进行 table 查找。)但它肯定不会那么好,因为它在循环内进行了所有额外的洗牌。 :/

这种向量化在 Zen1 上失败了,因为 SIMD ALU 只有 256 位宽,但在 Intel 上应该是一个重要的胜利,也许在 Zen2 和更高版本上是一个胜利。


但看起来 clang 在 vpsadbw 的内部循环中扩大到 32 位计数,所以它并没有达到预期的那么好。 1024x uint64_t 是输入数据的 256 __m256i 个向量,clang 展开 4 所以任何一个元素中的最大计数只有 64,不会溢出。

考虑到它所做的工作量,Clang 展开的数量惊人。 vextracti128vpackusdw 对我来说没有多大意义,IDK 为什么它会在循环内这样做。没有溢出风险的矢量化的简单方法只是 vpsadbw -> vpaddqvpaddd,它已经在 8 字节块内使用 vpsadbw 进行水平字节总和。 (更好的方法是将它推迟到字节元素可能溢出之前,所以做一些 vpaddb。就像 How to count character occurrences using SIMD 一样,尽管字节计数器在那里只增加 0 或 1,而不是0 .. 8)

请参阅 , especially Wojciech Muła's big-array popcnt functions: https://github.com/WojciechMula/sse-popcount/ - clang 使用与 popcnt_AVX2_lookup 相同的策略,但使用一种效率低得多的方法来跨迭代累积结果。