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

z.k*_*arl 6 c++ gcc sse intrinsics type-punning

(编者注:这个问题最初是: 一个人应该如何访问__m128i对象的m128i_i8成员或一般成员?,试图对GCC的定义使用MSVC特定的方法__m128i。但这是一个XY问题,并且已被接受。答案是关于XY问题。另一个答案确实回答了这个问题。)

我意识到Microsoft建议不要直接访问这些对象的成员,但是我需要对其进行设置,因此非常缺乏文档

我继续收到错误“我在'(我的var名)'中请求成员'm128i_i8',它是非类类型'wirelabel {aka __vector(2)long long int}'”的错误,因为我已包含所有正确的标头,并且可以识别__m128i变量。

注意1:wirelabel是__m128i的typedef,即在标头中存在

typedef __m128i wirelabel 
Run Code Online (Sandbox Code Playgroud)

注意2:使用注释 1的原因在下面的其他问题中进行了解释: tbb :: cache_aligned_allocator:通过__m128i获取“对成员的请求...非类类型的请求”。用户错误或错误?

注意3:我正在使用编译器g ++

注意4:以下问题不能回答我的问题,但会讨论相关信息为什么不直接访问__m128i字段?

我也知道有一个_mm_set_epi8函数,但是它要求您一次设置所有8位部分,而这对我来说目前不是一个选择。


接受答案的问题回答:

编辑:有人问我为什么我需要访问__m128i对象的16个8位部分中的每个细节,这是为什么:我有一个bool大小为'n * 128' 的数组(n为size_t ),我需要将它们存储在大小为'n'的'wirelabel'数组中。

现在,由于wirelabel只是__m128i的别名/ typedef(如果有区别,请纠正我),因此可以将128个布尔的“ n”个索引中的每个存储在“ wirelabel”数组中。

但是,为了做到这一点,我相信需要将每个8位转换为它的带符号等效项,并将其存储在数组中每个“ wirelabel”指针中的正确8位索引中。

Pet*_*des 4

那么你的源数据是连续的?您应该使用_mm_load_si128向量类型的标量分量,而不是乱搞。


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

pmovmskb( _mm_movemask_epi8) 对于每字节输入提取一位非常有用。您只需安排将您想要的位放入高位即可。

显而易见的选择是移位,但向量移位指令会竞争与pmovmskbHaswell 上相同的执行端口(端口 0)。(http://agner.org/optimize/)。相反,对于 的输入,加法0x7F将产生0x80(高位设置)1,但0x7F对于 的输入,将产生(高位清除)0。(boolx86-64 System V ABI 中的 a 必须作为整数 0 或 1 存储在内存中,而不仅仅是 0 与任何非零值)。

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

#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

因为我们希望在写入此位图时使用标量存储,所以出于严格别名的原因,我们希望dst使用标量存储。uint16_t使用 AVX2,您会想要uint32_t. (或者如果你确实combine = tmp1 << 16 | tmp将两个pmovmskb结果。但可能不会这样做。)

为了处理严格锯齿问题,如果您想稍后使用不同的 C 类型访问掩码位图,您可以使用memcpy这些存储,如另一个 Q&A中所示。

这会编译成这样的 asm 循环(使用 gcc7.3 -O3,在 Godbolt 编译器资源管理器上

.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
Run Code Online (Sandbox Code Playgroud)

所以这并不美妙(7 个熔断域 uops -> 每约 1.75 个时钟周期 16 个 bool 的前端瓶颈)。Clang 按 2 展开,每 1.5 个周期应管理 16 个 bool。


在 Haswell 上使用 shift ( pslld xmm0, 7) 只会每 2 个周期运行一次迭代,在端口 0 上出现瓶颈。这在 Skylake 及更高版本上不是问题;移位可以在更多端口上运行,因此这_mm_slli_epi32(v, 7)很好,并且避免需要向量常数。另请参阅 提取 __m128i 中每个布尔字节的低位?布尔数组到打包位图