提问人:Vitali 提问时间:9/7/2023 最后编辑:Peter CordesVitali 更新时间:9/10/2023 访问量:184
x86-64 SIMD 机制“比较”8 位无符号整数,给出 +1 / 0 / -1 结果(符号)的向量?
x86-64 SIMD mechanism to "compare" 8-bit unsigned integers, giving a vector of +1 / 0 / -1 results (signum)?
问:
假设我有两个无符号整数(8 位)打包寄存器 a 和 b。我想比较它们并返回 +1 表示 > b,0 表示 a=b,或返回 -1 表示< b。或者,距离也可以工作(即返回实际差值而不是 -1/+1)。
SIMD(大概是 AVX2)如何有效地实现这一点?我不能使用 AVX512,但很高兴知道这是否是 AVX512 功能。
答:
a <=> b
对于有符号字节将是(使用有符号饱和度减去)。
结果可以被 normailzed 用于_mm256_subs_epi8(a, b)
-1 / 0 / 1
_mm256_sign_epi8(_mm256_set1_epi8(-1), res)
无符号字节可以“范围转换”[1]到有符号字节:
const __m256i x80 = _mm256_set1_epi8(-128);
__m256i res = _mm256_subs_epi8(_mm256_xor_si256(a, x80), _mm256_xor_si256(b, x80));
绝对差异可以通过以下方式找到
_mm256_or_si256(_mm256_subs_epu8(a, b), _mm256_subs_epu8(b, a))
或
_mm256_sub_epi8(_mm256_max_epu8(a, b), _mm256_min_epu8(a, b))
.
[1] 范围转换:
SSE2(或更高版本)通常具有有符号整数的指令,而没有任何相应的无符号整数指令(反之亦然)。
要在有符号和无符号范围之间转换 8 位整数,只需添加或减去 128。
Signed 8-bit integers have the range -128..127
Unsigned 8-bit integers have the range 0..255
这种转换的微优化是使用 XOR 翻转通道的最高有效位。128 是 (0x80) 在 2 的补码中。没有第 9 位,因此无需借用或携带。在某些体系结构上,逻辑运算可能在比算术运算更多的执行端口上运行。大多数 SSE2 指令都具有破坏性的双操作数形式,因此 XOR 允许编译器自由地将 x ^= y 重新排序为 y ^= x,这与减法不同。0b10000000
评论
or(sub, sub)
sub_epu8
subs_epu8
sub_epi8
vpsubusb
vpminub
psubusb
sub
or
_mm256_cmpeq_epi8_mask
_mm256_cmplt_epi8_mask
_mm256_cmpgt_epi8_mask
_mm256_cmplt_epi8_mask
__m256i
__mmask32
cmpeq
这令人惊讶地乏味,因为无符号 8 位整数没有得到很好的支持,例如没有无符号比较。对于有符号的 8 位,这将是相当简单的。
使用英特尔内部函数,我想出了这个例程:
#include <immintrin.h>
#include <stdint.h>
#include <stddef.h>
void compare(int8_t* out, const uint8_t* a, const uint8_t* b, ptrdiff_t n)
{
ptrdiff_t i;
for(i = 0; i + 32 <= n; i += 32) {
__m256i ai = _mm256_loadu_si256((const __m256i*)(a + i));
__m256i bi = _mm256_loadu_si256((const __m256i*)(b + i));
__m256i min_u = _mm256_min_epu8(ai, bi);
/* Set all bits 1 (== -1) if a <= b */
__m256i a_le_b = _mm256_cmpeq_epi8(min_u, ai);
/* And the reverse */
__m256i b_le_a = _mm256_cmpeq_epi8(min_u, bi);
/*
* Three cases:
* 1. a == b: Both masks are set. Subtraction results in zero
* 2. a < b: Left mask is -1, right is 0. Result is -1
* 3. a > b: Left mask is 0, right is -1. 0 - (-1) == 1
*/
__m256i result = _mm256_sub_epi8(a_le_b, b_le_a);
_mm256_storeu_si256((__m256i*)(out + i), result);
}
if(n > 32) {
/* overlapping iteration to deal with trailing elements */
i = n - 32;
__m256i ai = _mm256_loadu_si256((const __m256i*)(a + i));
__m256i bi = _mm256_loadu_si256((const __m256i*)(b + i));
__m256i min_u = _mm256_min_epu8(ai, bi);
__m256i a_le_b = _mm256_cmpeq_epi8(min_u, ai);
__m256i b_le_a = _mm256_cmpeq_epi8(min_u, bi);
__m256i result = _mm256_sub_epi8(a_le_b, b_le_a);
_mm256_storeu_si256((__m256i*)(out + i), result);
}
else if(n < 32) {
/* Scalar loop if entire input is less than 1 vector */
for(; i < n; ++i) {
uint8_t ai = a[i], bi = b[i];
out[i] = (ai >= bi) - (ai <= b);
}
}
}
主循环的主体编译为:
vmovdqu ymm1, YMMWORD PTR [rdx+rax]
vmovdqu ymm0, YMMWORD PTR [rsi+rax]
vpminub ymm2, ymm0, ymm1
vpcmpeqb ymm0, ymm0, ymm2
vpcmpeqb ymm1, ymm1, ymm2
vpsubb ymm0, ymm0, ymm1
vmovdqu YMMWORD PTR [rdi+rax], ymm0
GCC 在给定标量循环时,会提出相同的解决方案。但它对循环的内容非常敏感。它不能将 if-else-if-else 构造(或链接的三元运算符)简化为相同的代码。
AVX512系列
AVX512 使代码更加简单明了。这是 AVX512 的主循环
__m512i ai = _mm512_loadu_si512((const __m512i*)(a + i));
__m512i bi = _mm512_loadu_si512((const __m512i*)(b + i));
__mmask64 a_ne_b = _mm512_cmp_epu8_mask(ai, bi, _MM_CMPINT_NE);
__mmask64 a_gt_b = _mm512_cmp_epu8_mask(bi, ai, _MM_CMPINT_LT);
/* -1 if a != b */
__m512i result = _mm512_movm_epi8(a_ne_b);
/* negate -1 to 1 if a > b */
result = _mm512_mask_abs_epi8(result, a_gt_b, result);
_mm512_storeu_si512((__m512i*)(out + i), result);
评论
vpcmpub
_mm256_cmp_epu8_mask(a, b, _MM_CMPINT_LT)
min
sub
vpcmpgtb
min
eq
vpblendvb
vpternlogd
-march=haswell
-march
__m256i signum_compare (__m256i a, __m256i b) { __m256i maxab = _mm256_max_epu8 (a, b); __m256i maxa = _mm256_cmpeq_epi8 (maxab, a); __m256i maxb = _mm256_cmpeq_epi8 (maxab, b); return _mm256_sub_epi8 (maxb, maxa); }
vpmovm2b
0
-1
_mm512_mask_abs_epi8
vpsignb
首先,您很少真正想要或需要具有 + / 0 / - 值的向量;使用它可能需要将其解码回两个掩码或其他东西,因此只需比较两次即可获得两个掩码。(例如 / 获得大于或小于向量,并直接在两个输入上获得相等向量。_mm256_min_epu8
_mm256_cmpeq_epi8
cmpeq
如果要使用它来有条件地否定或将另一个字节向量归零,请使用 aqrit 的答案的结果,该结果生成 +/0/- 向量。(令人惊讶的是,没有 AVX-512 ,所以在这种情况下你需要其他东西。_mm256_sign_epi8
vpsignb
如果你真的想有条件地否定一些浮点数或其他东西,不要乘以 1.0 或 -1.0,只需用 XOR 翻转符号位即可。(并用 0 或 0xffffffff 掩码,在用 符号扩展结果后 )。vpcmpeqb
vpmovsxbd
既然您询问了 AVX-512 版本:
我上面说的关于首先不创建 +/0/- 向量的内容对 AVX-512 来说是双倍的;使用其他操作的屏蔽版本可以让您几乎免费地应用 A,或者使用更少的指令,无论您实际想用它做什么。也就是说,我们可以看看它:__mmask64
AVX-512 的新无符号比较指令只能比较到掩码寄存器中,并将其具体化为 0 / -1 的向量需要另一条指令。(但至少这是一个廉价的指令,例如在英特尔的任何执行端口上运行,并且不需要已经加载任何向量常量。在 AVX-512 之前,唯一可用的 SIMD 整数比较是 equality 和 signed greater-than。(但是有符号和无符号的最小/最大指令,以及有符号和无符号饱和减法。
使用 AVX-512 做到这一点的一种方法仍然是比 aqrit 的指令更多,但会产生 0 / -1。
2x 比较(无符号和 )得到 2 个掩码。
ne
gt
vpmovm2b
从掩码中获取 / 对于 / 不相等的元素。0
-1
ne
使用掩码进行合并遮罩以否定我们想要 +1 而不是 -1 的元素。
_mm512_mask_abs_epi8
gt
(参见 Homer512 的答案,了解 C 的内在版本;感谢您在评论中写下我发布的建议。以及该答案的早期修订版,用于使用合并掩码来混合 a 和 a 常量的版本。set1(-1)
set1(1)
不幸的是,英特尔 CPU 只能在一个执行端口上运行 compare-into-mask,因此它没有我们想要的指令级并行性。但是,只要比较先运行,就可以了;在结果准备好之前,我们不能使用掩码,因此可以与第二次比较同时进行。ne
gt
vpmovm2b
在 AVX-512 中使用 256 位向量宽度(除非您的整个程序大量使用 512 位向量,这通常是一个好主意),您可以使用 AVX2 和按位 NOT ( with ) 实现向量,以获得 2 个周期的延迟,但代价是需要一个向量常数。(https://uops.info/)。 然而,实现起来很便宜,只是一个针对自己的寄存器。ne
vpcmpeqb
vpxor
-1
-1
vpcmpeqd
但是,对于 256 位向量,我认为具有 4 个 AVX2 指令的 Homer 版本可能是最好的,并且与这两种方式进行比较,然后减去不相交的掩码以否定其中一个中的元素,同时合并它们。我看不出有什么方法可以用 AVX-512 击败它。或者更好的是,如果你不需要 +1/0/-1,aqrit 的符号饱和减法会得到一个 +/0/- 向量。vpminub
-1
评论
-1
0
+1
_mm_sign_epi8
>
==
_mm256_cmpgt_epi8