AVX2 / gcc:通过使用不同的寄存器来提高CPU级并行性

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

我有这个代码:

__attribute__((target("avx2")))
size_t lower_than_16(const uint64_t values[16], uint64_t x)
{
    __m256i vx = _mm256_set1_epi64x(x);
    __m256i vvals1 = _mm256_loadu_si256((__m256i*)&values[0]);
    __m256i vvals2 = _mm256_loadu_si256((__m256i*)&values[4]);
    __m256i vvals3 = _mm256_loadu_si256((__m256i*)&values[8]);
    __m256i vvals4 = _mm256_loadu_si256((__m256i*)&values[12]);
    __m256i vcmp1  = _mm256_cmpgt_epi64(vvals1, vx);
    __m256i vcmp2  = _mm256_cmpgt_epi64(vvals2, vx);
    __m256i vcmp3  = _mm256_cmpgt_epi64(vvals3, vx);
    __m256i vcmp4  = _mm256_cmpgt_epi64(vvals4, vx);
    const int mask = (_mm256_movemask_pd((__m256d)vcmp1)) |
                    (_mm256_movemask_pd((__m256d)vcmp2) << 4) |
                    (_mm256_movemask_pd((__m256d)vcmp3) << 8) |
                    (_mm256_movemask_pd((__m256d)vcmp4) << 12);
    if (mask != 0xFFFF) {
        // found
        return __builtin_ctz(~mask);
    }

    return 16;
}

基本上给出了一个包含 16 个元素的数组,我想找到第一个元素的索引,其中

values[i] <= x
为 true。如果找不到元素,则返回 16。

这是用 AVX2 实现的,我使用 gcc 作为编译器。装配体如下所示:

lower_than_16:
        vmovq   xmm2, rsi
        vmovdqu ymm1, YMMWORD PTR [rdi]
        vpbroadcastq    ymm0, xmm2
        vpcmpgtq        ymm1, ymm1, ymm0
        vmovmskpd       esi, ymm1
        vmovdqu ymm1, YMMWORD PTR [rdi+32]
        vpcmpgtq        ymm1, ymm1, ymm0
        vmovmskpd       eax, ymm1
        vmovdqu ymm1, YMMWORD PTR [rdi+64]
        sal     eax, 4
        vpcmpgtq        ymm1, ymm1, ymm0
        vmovmskpd       ecx, ymm1
        vmovdqu ymm1, YMMWORD PTR [rdi+96]
        sal     ecx, 8
        vpcmpgtq        ymm0, ymm1, ymm0
        or      eax, ecx
        or      eax, esi
        vmovmskpd       edx, ymm0
        sal     edx, 12
        or      eax, edx
        mov     edx, 16
        cmp     eax, 65535
        je      .L1
        not     eax
        xor     edx, edx
        rep bsf edx, eax
.L1:
        mov     rax, rdx
        vzeroupper
        ret

(可以在这里看到:https://godbolt.org/z/7eea39Gqv

我看到

gcc
总是为每个展开的迭代使用相同的寄存器。然而,如果每个展开的迭代使用不同的
ymm
寄存器,不是会更高效吗?因为这样 CPU 可以更轻松地并行执行这 4 个独立比较?我知道 CPU 会进行一些寄存器重命名,但它是否足够聪明,不会强制这些指令不并行执行?或者如果使用不同的寄存器会更容易/更高效吗?

非常感谢

gcc vectorization cpu-architecture simd avx2
1个回答
0
投票

每个寄存器写入均已重命名。 GCC 寄存器分配选择没有任何缺点。

当寄存器文件大小是乱序执行程序可以看到多远的限制因素时1,指令是否写入“冷”寄存器或覆盖最近的结果并不重要. (我没有尝试专门测试它。我从未在Agner Fog或英特尔的优化手册或其他任何地方看到过这样或那样的建议。)

物理寄存器文件 (PRF) 中的条目无法释放,直到覆盖它的指令退休(来自重新排序缓冲区,又称为 ROB),因为任何时候的外部中断都可能丢弃非退休指令并回滚到退休状态。 (除非有一些我忘记或不知道的技巧可以允许提前释放条目。)

如果 FLAGS 的两个部分(CF 和 SPAZO 组)是由单独的指令编写的(例如在

inc
之后),则将引用两个单独的 PRF 条目。 像
add
cmp
shl
这样写入所有内容的指令将在退出时释放这两个条目。 但整数和 FP/SIMD 有单独的寄存器文件,因此您无需担心使用
inc
dec
作为循环条件在 SIMD 循环中保留 FLAGS 分割。 而且 FLAGS 经常用整数代码编写,我认为这很少是一个问题,或者至少不是一个你可以在不花费更多指令的情况下做任何事情的问题,这更糟糕。 但有时这可能是使用 add 1
 而不是 
inc
 的一个非常小的
原因。
(在正常的 x86 设计中,整数 PRF 条目有足够的空间来保存 64 位整数结果完整的 FLAGS 结果。因此,在跟踪输入和输出方面,像
add
这样的指令仍然只有一个输出。所以CF、SPAZO 和 R15 等寄存器都可以将其 RAT 条目指向同一 PRF 条目。直到所有引用都被释放后,PRF 条目才能被释放。死了。)


您可能认为mov-elimination(在重命名期间处理而不是通过执行单元处理)让两个寄存器指向相同的 PRF 条目会有所帮助。 事实上它可能会这样做,但不幸的是,移动消除槽对此类双倍条目进行引用计数的能力有限,特别是在具有该功能的前几代CPU中(Ivy Bridge)。
因此,通常您希望尽快覆盖

mov
的结果,以释放资源以消除未来的
mov
指令。 例如
mov eax, ecx
/
or eax, 0x55
修改新副本,而不是修改原始副本,除非您很快也将用其他内容覆盖副本。 但 Ice Lake 通过微代码更新禁用了整数 mov-elim,因此
mov
具有非零延迟,并且(其他条件相同)在针对通用或 Ice Lake 进行调整时仍应远离关键路径。


脚注 1:PRF 容量与 ROB 容量等限制因素:


附注Intel 的 P6 系列(Nehalem 及更早版本)具有不同的效果,这对您选择使用寄存器的方式有一定影响:寄存器读取停顿,其中每个寄存器可以读取的“冷”(不是最近写入的)寄存器数量有限制。时钟周期,在分配/重命名/发布期间。 它没有 PRF:它将结果保存在 ROB 本身中,但必须从“退休寄存器文件”中读取冷值。 但这与您是否选择多次覆盖多个不同的规则与多次覆盖相同的规则无关;重要的是写入和读取之间的距离。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.