我在 Intel Intrinsic 指南中看不到这一点,但也许我错过了它。
如果我有两个 512 位寄存器
a
和 b
我想将它们视为具有四个 128 位元素,然后执行:
a[0] + b[0]
a[1] + b[1]
a[2] + b[2]
a[3] + b[3]
这样的AVX 512指令存在吗?
不,任何数学运算的最宽元素大小都是 64 位。 AVX-512 具有无符号整数比较,因此您可以轻松生成具有进位的元素掩码 (
carry = (a+b) < a
)。
然后可以移动掩码 (
kshiftlb
) 并将其用于合并掩码 add
(或 sub
的 -1
)以递增低半部分已进位的元素的高 64 位。 (使用 _mm512_setr_epi64(0, -1, 0, -1, 0, -1, 0, -1)
,因此从高半部分进位会将 0
添加到下一个 128 位元素的低半部分,因此您不必担心跨元素边界移动进位。)set1(-1)
的成本很低生成,但编译器可能不太擅长使用 0, -1
/ vpternlogq
的 vpslldq zmm, zmm, 8
模式,因此您可能只使用 add
与 0, 1
模式。
或者也许是其他一些技巧,也许是在向量寄存器中生成
0
或 -1
并随机播放,而不是通过掩码寄存器? 但饱和减法仅适用于 8 或 16 位元素。
与单个 BigInt 添加的主要区别在于,每个进位只能传播一步,而不是一直传播到最后,因此不存在长串行依赖性。
#include <immintrin.h>
__m512i add_epi128(__m512i a, __m512i b)
{
__m512i sum = _mm512_add_epi64(a, b);
__mmask8 carry = _mm512_cmplt_epu64_mask(sum, b); // a or b doesn't matter, but compilers don't realize that.
// For the standalone function, using b lets them overwrite a in ZMM0
// Of course in reality you want this function to inline.
//carry = _kshiftli_mask8(carry, 1); // or actually just kaddb is more compact, but compilers miss the optimization
carry = _kadd_mask8(carry, carry);
//carry += carry;
const __m512i high_ones = _mm512_setr_epi64(0, -1, 0, -1, 0, -1, 0, -1);
// Carry-propagation into the high half of each u128, with merge-masking
sum = _mm512_mask_sub_epi64(sum, carry, sum, high_ones);
return sum;
}
kaddb
和 kshiftlb
在 Intel 上都有 4 个周期延迟,在 Zen 4 上有 1 个周期延迟 (https://uops.info/),但 kaddb
短了一个字节(没有立即数)。 我必须使用内在函数来获取 GCC 并 clang 来发出它而不是 kshiftlb
。 (神箭)
# clang 18 -O3 -march=x86-64-v4
.LCPI0_0:
.quad 0
.quad 1
...
add_epi128(long long vector[8], long long vector[8]):
vpaddq zmm0, zmm1, zmm0
vpcmpltuq k0, zmm0, zmm1
kaddb k1, k0, k0
vpaddq zmm0 {k1}, zmm0, zmmword ptr [rip + .LCPI0_0]
ret
假设将向量内联并保持在寄存器中,则对于前端和后端执行单元来说,这是 4 个微指令。 从两个向量输入准备就绪起,英特尔的关键路径延迟约为 10 个周期。 (1 表示
vpaddq
指令,即使有合并掩码。4 或 3 表示比较掩码,Zen4 上为 5。4 表示 kaddb
,英特尔上 vs. Zen4 上的 1。)