提取 __m128i 中每个 bool 字节的低位?布尔数组到打包位图

Extract the low bit of each bool byte in a __m128i? bool array to packed bitmap

(编者按:这个问题最初是:如何访问 __m128i object 的 m128i_i8 成员或一般成员?,试图在 GCC 的 __m128i 定义上使用 MSVC-specific 方法。但这是一个 XY 问题,接受的答案是关于这里的 XY 问题。另一个答案 确实如此 回答这个问题。)

我知道 Microsoft 建议不要直接访问这些 object 的成员,但我需要设置它们并且 documentation 非常缺乏。

我继续收到我不明白的错误 "request for member ‘m128i_i8’ in ‘(my var name)', which is of non-class type ‘wirelabel {aka __vector(2) long long int}’",因为我包含了所有正确的 header,它确实识别 __m128i 变量。

注意 1:wirelabel 是 __m128i 的类型定义,即存在于 header

typedef __m128i wirelabel 

注2:使用注1的原因在下面的其他问题中有解释: tbb::cache_aligned_allocator: Getting "request for member...which is of non-class type" with __m128i. User error or bug?

注3:我使用的是编译器g++

注4:以下问题没有回答我的问题,但讨论了相关信息Why should you not access the __m128i fields directly?

我也知道有一个 _mm_set_epi8 函数,但它要求您一次设置所有 8 位部分,目前我不适合这个选项。


接受的答案回答的问题:

编辑:我被问及为什么我认为我需要访问 __m128i object 的 16 个 8 位部分中的每一个部分的更多细节,原因如下:我有一个大小为 'n*128' 的 bool 数组(n 是一个 size_t),我需要将它们存储在一个大小为 'n'.[=18 的 'wirelabel' 数组中=]

现在因为 wirelabel 只是 __m128i 的 alias/typedef(如果有差异请纠正我),128 个布尔值的每个 'n' 索引都可以存储在 'wirelabel'数组.

但是,为了做到这一点,我认为需要将每 8 位转换成它的有符号等价物,并将其存储在数组中每个 'wirelabel' 指针的正确 8 位索引中。

创建一个匿名联合,其中包含一个 _m128i 成员和一个您要设置其成员的其他类型的数组。 Type-punning 在 C 中是合法的,在 g++、clang++ 和 MSVC 中作为扩展支持。如果要设置单个位,可以将其他成员声明为位域的 struct。位域的顺序是 implementation-defined,但无论如何您都在使用 Intel 内在函数,所以它将是 little-endian。

所以你的源数据是连续的?您应该使用 _mm_load_si128 而不是乱用矢量类型的标量分量。


您真正的问题是将 bool 的数组(x86 上 g++ 使用的 ABI 中每个元素 1 个字节)打包到位图中。您应该使用 SIMD 执行 this,而不是使用标量代码一次设置 1 位或字节。

pmovmskb (_mm_movemask_epi8) 非常适合从输入的每个字节中提取一位。你只需要安排把你想要的位放到高位就可以了。

显而易见的选择是移位,但是向量移位指令竞争与 Haswell 上的 pmovmskb 相同的执行端口(端口 0)。 (http://agner.org/optimize/)。相反,添加 0x7F 将为 1 的输入生成 0x80(高位设置),但对于 0 的输入将生成 0x7F(高位清除) . (并且 x86-64 系统 V ABI 中的 bool 必须作为整数 0 或 1 存储在内存中,而不仅仅是 0 与任何 non-zero 值)。

为什么不 pcmpeqb 对抗 _mm_set1_epi8(1)? Skylake 运行s pcmpeqb 在端口 0/1 上,但 paddb 在所有 3 个向量 ALU 端口 (0/1/5) 上。不过,在 pcmpeqb/w/d/q 的结果上使用 pmovmskb 是很常见的。

#include <immintrin.h>
#include <stdint.h>

// n is the number of uint16_t dst elements
// We access n*16 bool elements from src.
void pack_bools(uint16_t *dst, const bool *src, size_t n)
{
     // you can later access dst with __m128i loads/stores

    __m128i carry_to_highbit = _mm_set1_epi8(0x7F);
    for (size_t i = 0 ; i < n ; i+=1) {
        __m128i boolvec = _mm_loadu_si128( (__m128i*)&src[i*16] );
        __m128i highbits = _mm_add_epi8(boolvec, carry_to_highbit);
        dst[i] = _mm_movemask_epi8(highbits);
    }
}

因为我们想在写这个位图时使用标量存储,所以我们希望 dstuint16_t 中,原因是 strict-aliasing。使用 AVX2,您需要 uint32_t。 (或者,如果您 combine = tmp1 << 16 | tmp 合并了两个 pmovmskb 结果。但可能不会这样做。)

这会编译成这样的 asm 循环 (with gcc7.3 -O3, on the Godbolt compiler explorer)

.L3:
    movdqu  xmm0, XMMWORD PTR [rsi]
    add     rsi, 16
    add     rdi, 2
    paddb   xmm0, xmm1
    pmovmskb        eax, xmm0
    mov     WORD PTR [rdi-2], ax
    cmp     rdx, rsi
    jne     .L3

所以这并不好(7 fuse-domain uops -> front-end 瓶颈在每 ~1.75 个时钟周期 16 个布尔值。 Clang 展开 2,并且每 1.5 个周期应该管理 16 个布尔值。

使用移位 (pslld xmm0, 7) 只会 运行 在 Haswell 上每 2 个周期进行一次迭代,在端口 0 上出现瓶颈。