我有这个代码:
__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
。如果找不到元素,则返回 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 会进行一些寄存器重命名,但它是否足够智能,不会强制这些指令不并行执行?或者,如果使用不同的寄存器,会不会更容易/更高效?
多谢
每个寄存器写入都会被重命名,并且没有 CPU 对每个时钟周期多次重命名同一寄存器有任何限制。例如,如果所有指令都写入
XMM0
或EFLAGS
或RCX
或其他内容,则前端吞吐量不会降低。(至少没有 x86 CPus;Agner Fog 的微架构指南专门提到了他测试过的每一个。)这里 GCC 寄存器分配选择没有任何缺点。
当寄存器文件大小是无序执行可以看到1 的限制因素时,指令是否写入“冷”寄存器或覆盖最新结果并不重要。 (我没有尝试专门测试它。 我从未在 Agner Fog 或英特尔的优化手册或其他任何地方看到过这种或那种建议。)
物理寄存器文件 ( PRF )中的条目无法释放,直到覆盖它的指令退出(从重排序缓冲区,又称 ROB),因为任何时候的外部中断都可能丢弃未退出的指令并回滚到退出状态。(除非我忘记或不知道有什么技巧可以提前释放条目。)
如果 FLAGS 的两个部分(CF 和 SPAZO 组)是由单独的指令写入的(例如在 之后),则会引用两个单独的 PRF 条目。 诸如、或 之类写入所有这些的
inc
指令将在退出时释放两个条目。 但整数和 FP/SIMD 具有单独的寄存器文件,因此您不必担心在使用或作为循环条件的 SIMD 循环中将 FLAGS 拆分。 而且 FLAGS 经常以整数代码编写,因此我认为这很少成为问题,或者至少您无法在不花费更多指令的情况下对此采取任何措施,而这更糟糕。 但有时它可能是使用而不是 的一个非常小的原因。 (在正常的 x86 设计中,整数 PRF 条目有足够的空间来容纳 64 位整数结果和完整的 FLAGS 结果。因此在跟踪输入和输出方面,像这样的指令仍然只有一个输出。因此 CF、SPAZO 和像 R15 这样的寄存器都可以让它们的 RAT 条目指向同一个 PRF 条目。在所有引用都失效之前,PRF 条目不能被释放。)add
cmp
shl
inc
dec
add 1
inc
add
您可能会认为mov-elimination(在重命名期间处理而不是通过执行单元处理)让两个寄存器指向同一个 PRF 条目会有所帮助。它实际上可能会这样做,但不幸的是,mov-elimination 槽的容量有限,无法引用计数这样的重复条目,特别是在具有该功能的前几代 CPU(Ivy Bridge)中。
所以通常你想覆盖
mov
很快的结果,以释放资源来消除未来的mov
指令。例如mov eax, ecx
/or eax, 0x55
修改新副本,而不是修改原始副本,除非您也要用其他东西覆盖该副本。但是 Ice Lake 通过微码更新禁用了整数 mov-elim,因此mov
具有非零延迟,并且(在其他所有条件相同的情况下)在针对通用或 Ice Lake 进行调整时仍应远离关键路径。脚注 1:PRF 容量与 ROB 容量等限制因素:
ps Intel 的 P6 系列(Nehalem 及更早版本)具有不同的效果,这会对您选择使用寄存器的方式产生一定影响:寄存器读取停滞,在分配/重命名/发布期间,每个时钟周期可以读取的“冷”(不是最近写入的)寄存器数量受到限制。它没有 PRF:它将结果保存在 ROB 本身中,但必须从“退出寄存器文件”中读取冷值。但这与您选择多次覆盖多个不同的寄存器还是相同的寄存器无关;重要的是写入和读取之间的距离。