使用 AVX/AVX2/SSE __m128i 将所有负字节设置为 -128 (0x80) 并保留所有其他字节

With AVX/AVX2/SSE __m128i set all bytes that are negative to -128 (0x80) and leave all other bytes alone

基本上我想做的是获取一个 __m128i 寄存器并将每个负字节的值设置为 -128 (0x80) 并且不更改任何正值。

确切是:

signed char __m128_as_char_arr[16] = {some data};
for(int i = 0; i < 16; i++) {
     if (__m128_as_char_arr[i] < 0) { //alternative __m128_as_char_arr[i] & 0x80
           __m128_as_char_arr[i] = 0x80;
     }

}

我认为最好的方法是:

__m128i v = some data;
int mask = _mm_movemask_epi8(_mm_cmpgt_epi8(_mm_set1_epi8(0xff), v));

// use mask in some way to only set chars with 1s bit set

但我不知道 (1) 使用什么指令来仅设置与 mask 相关的字节以及 (2) 是否有更好的方法来做到这一点(根本没有掩码或者更好的生成掩码的方法)。

更新:@PaulR 想出了一个更好的主意。接受那个答案。 _mm_min_epu8(1 uop)至少和 _mm_blendv_epi8(最多 2 uops)一样便宜,并且只需要 SSE2。


不如_mm_min_epu8好,留在这里以防min技巧不完全有效的相关情况。

SSE4.1(因此 AVX 及更高版本)具有 a variable-blend that selects based on the top bit of each byte。您可以将矢量用作混合控件和数据输入之一。

// SSE4.1 or AVX1.  Or for __m256i, AVX2
__m128i  negative_to_min(__m128i v){
    // take 2nd operand for elements of v where the high bit is set
    return _mm_blendv_epi8(v, _mm_set1_epi8(0x80), v);
}

仅使用 SSE2,您需要 0 > vpcmpgtb 来识别负元素。直接的方法是通常的 AND/ANDN/OR 在没有 pblendvb 的情况下混合,但我们可以更聪明,因为结果的最高位总是与输入的最高位匹配,并且我们想要的结果否定的情况实际上是 x & 0x80.

                   // negative        non-neg
m = 0x80 ^ (0>x);  // 0x80             0x7f
x &= m;            // x&0x80 = 0x80    x & 0x7f = x
// SSE2
__m128i  negative_to_min(__m128i v)
{
    __m128i  neg = _mm_cmpgt_epi8(_mm_setzero_si128(), v);    // neg        non-neg
    __m128i  mask = _mm_xor_si128(neg, _mm_set1_epi8(0x80));  // 0x80   or  0x7f
    return   _mm_and_si128(mask, v);
}

这是更少的指令 (3),并且关键路径延迟不比 PCMPGTB / AND / ANDN / OR 差。它也不应该需要任何额外的 movdqa 指令,如果它用 pxor xmm0,xmm0 廉价地生成一个零向量,然后将其覆盖为 pcmpgtb 目的地。

如果您在其他地方使用 0x7f 而不是 0x80 常量,您可以与 0x7f 异或并使用 _mm_andn_si128(mask, v); 作为最后一步,以反转面具。否则,最好使用交换操作让编译器更容易优化。


回复:您的方法:如果没有 AVX512,movemask 就不是一个有用的构建块。没有 SIMD 方法可以将位图与矢量一起使用。在 AVX512 生成向量掩码而不是位掩码之前比较指令/内在函数,因此您可以将它们与 AND/ANDN/XOR/OR 按位运算一起使用。

此外,您的 -1 > v 会将 -1 误认为是非负数。

您可以将值视为无符号并使用最小运算(_mm_min_epu8 ),例如

v = _mm_min_epu8(v, _mm_set1_epi8(128));

除了是一条廉价的指令外,它还适用于 SSE2 及更高版本。