【问题标题】:Extract the low bit of each bool byte in a __m128i? bool array to packed bitmap提取 __m128i 中每个 bool 字节的低位?布尔数组到打包位图
【发布时间】:2018-08-22 03:24:54
【问题描述】:

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

我知道微软建议不要直接访问这些对象的成员,但我需要设置它们,而 documentation 非常缺乏。

我继续收到错误“request for member 'm128i_i8' in '(my var name)', which is non-class type 'wirelabel {aka __vector(2) long long int}'”我没有理解,因为我已经包含了所有正确的标题,并且它确实识别 __m128i 变量。

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

typedef __m128i wirelabel 

Note2:使用 Note1 的原因在以下其他问题中进行了解释: 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 对象的 16 个 8 位部分中的每一个,原因如下:我有一个大小为 ' 的 bool 数组n*128'(n 是 size_t),我需要将它们存储在大小为 'n' 的 'wirelabel' 数组中。

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

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

【问题讨论】:

标签: c++ gcc sse intrinsics type-punning


【解决方案1】:

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


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

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

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

为什么不pcmpeqb 对抗_mm_set1_epi8(1)? Skylake 在端口 0/1 上运行 pcmpeqb,但在所有 3 个向量 ALU 端口 (0/1/5) 上运行 paddb。不过,在 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);
    }
}

因为我们想在编写这个位图时使用标量存储,我们希望dst 位于uint16_t 中,因为严格混叠的原因。使用 AVX2,您需要uint32_t。 (或者如果您使用combine = tmp1 &lt;&lt; 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 个保险丝域微指令 -> 前端瓶颈,每 ~1.75 个时钟周期 16 个布尔值)。 Clang 展开 2,每 1.5 个周期应管理 16 个布尔值。

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 这是一个超级酷的主意!我以前没有听说过 C 语言中的匿名联合,也没有听说过类型双关语。然而,在对这些主题进行一些研究之后,这个解决方案对我不起作用,就好像这些变量在这种情况下是独立使用的,后来它们被独立使用并且将它们放在一个联合中会导致一个覆盖另一个我的旧更改需要保留。
    • 对于 setting 向量,与_mm_set_epi8(highest, ..., lowest);(或_mm_setr_epi8(lowest, ..., highest))相比,联合没有优势。它可能会以不同的方式编译,但如果它编译为效率较低的代码比联合,这是编译器中的一个错过优化的错误。 (它确实发生了,值得一试;_mm_set_epi8 有很多元素,所以很糟糕。)但在@z.karl 的情况下,这听起来像数据是连续的,所以简单地@ 987654326@
    • @PeterCordes union 的优点是您可以自己设置单个元素。我怀疑类型双关代码会复制到内存并返回,而_mm_set_epi8() 是为了使用 SSE 指令设置整个向量。但我必须用-S 编译并检查生成的代码。
    • @z.karl 抱歉,它不适合您的需求。这是一个很好的工具。
    • 我没有考虑修改单个成员的情况。如果幸运的话,设置单个成员可能会编译为 pinsrb。但是如果没有 SSE4,是的,movdqa 存储/字节存储/movdqa 加载是可能的。对于更广泛的元素,您更有可能获得movd + shuffle 或其他东西来将数据合并到向量寄存器中。
    猜你喜欢
    • 2016-08-02
    • 2016-09-05
    • 2011-05-02
    • 1970-01-01
    • 1970-01-01
    • 2017-03-02
    • 1970-01-01
    • 2013-01-07
    • 1970-01-01
    相关资源
    最近更新 更多