提取 __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);
}
}
因为我们想在写这个位图时使用标量存储,所以我们希望 dst
在 uint16_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 上出现瓶颈。
(编者按:这个问题最初是:如何访问 __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);
}
}
因为我们想在写这个位图时使用标量存储,所以我们希望 dst
在 uint16_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 上出现瓶颈。