将打包的半字节组合成打包的字节
Combine packed nibbles into packed bytes
给定一个或多个 __m128i
或 __m256i
每个 16 位元素包含一个半字节,将它们组合并打包为每个 8 位元素一个字节的最快方法是什么(即 (hi << 4) | lo
对于相邻的 16 位元素)?
这是我想出的最好的方法,不幸的是,它与标量代码相当:
const static __m256i shufmask = _mm256_setr_epi8(
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255,
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255);
const static __m256i high4 = _mm256_setr_epi8(
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0
);
inline static int64_t hnib2byte(__m256i nibbles) { // (a << 4) | b;
// hi 0 lo 0, ...
__m256i upper = _mm256_slli_epi16(nibbles, 4);
// Align upper and lower halves so they can be ORed vertically
// lo 0 0 0, ...
__m256i lower = _mm256_shuffle_epi8(nibbles, shufmask);
// ab x x x, ...
__m256i or = _mm256_or_si256(upper, lower);
// Pack into bytes
or = _mm256_and_si256(or, high4);
__m256i pack16 = _mm256_packus_epi16(or, or);
const int _3to2 = 0b00001000;
__m256i perm16 = _mm256_permute4x64_epi64(pack16, _3to2); // :(
__m256i pack8 = _mm256_packus_epi16(perm16, perm16);
return _mm_cvtsi128_si64(_mm256_castsi256_si128(pack8));
}
包括 AVX2 在内的指令都是公平的。 AVX-512 中的掩码移位提供了更好的选择。这是在循环中调用的,因此尽早将半字节打包到 8 位元素中也是公平的游戏。
下面的解决方案 hnib2byte_v2
应该比您的解决方案快一点,至少在 Intel 处理器上是这样。
指令 vpermd
或内在 _mm256_permutevar8x32_epi32
在 AMD Ryzen 上运行缓慢。在那个平台上最好用_mm256_extracti128_si256
提取[=16=的高128位通道,用_mm256_castsi256_si128
提取低128位通道,把这两个结合起来_mm256_or_si256
获取最低 64 位的答案。
/*
gcc -O3 -m64 -Wall -mavx2 -march=broadwell nibble2byte.c
*/
#include <immintrin.h>
#include <stdio.h>
#include <stdint.h>
int print_avx2_hex(__m256i ymm);
inline static int64_t hnib2byte_v2(__m256i nibbles) {
__m256i shufmask8 = _mm256_set_epi8(-1,-1,-1,-1, -1,-1,-1,-1, 14,10,6,2, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 14,10,6,2);
__m256i shufmask32 = _mm256_set_epi32(7,7,7,7,7,7,5,0);
__m256i lower = _mm256_slli_epi32(nibbles, 20);
// 00E0000000C00000 00A0000000800000 0060000000400000 0020000000000000
__m256i up_lo = _mm256_or_si256(lower,nibbles);
// 00EF000E00CD000C 00AB000A00890008 0067000600450004 0023000200010000
__m256i pck = _mm256_shuffle_epi8(up_lo,shufmask8);
// 0000000000000000 EFCDAB8900000000 0000000000000000 0000000067452301
__m256i pck64 = _mm256_permutevar8x32_epi32(pck,shufmask32);
// 0000000000000000 0000000000000000 0000000000000000 EFCDAB8967452301
// print_avx2_hex(lower);
// print_avx2_hex(up_lo);
// print_avx2_hex(pck);
// print_avx2_hex(pck64);
return _mm_cvtsi128_si64(_mm256_castsi256_si128(pck64));
}
inline static int64_t hnib2byte(__m256i nibbles) { // (a << 4) | b;
__m256i shufmask = _mm256_setr_epi8(
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255,
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255);
__m256i high4 = _mm256_setr_epi8(
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0);
// hi 0 lo 0, ...
__m256i upper = _mm256_slli_epi16(nibbles, 4);
// Align upper and lower halves so they can be ORed vertically
// lo 0 0 0, ...
__m256i lower = _mm256_shuffle_epi8(nibbles, shufmask);
// ab x x x, ...
__m256i or = _mm256_or_si256(upper, lower);
// Pack into bytes
or = _mm256_and_si256(or, high4);
__m256i pack16 = _mm256_packus_epi16(or, or);
const int _3to2 = 0b00001000;
__m256i perm16 = _mm256_permute4x64_epi64(pack16, _3to2); // :(
__m256i pack8 = _mm256_packus_epi16(perm16, perm16);
return _mm_cvtsi128_si64(_mm256_castsi256_si128(pack8));
}
int print_avx2_hex(__m256i ymm)
{
long unsigned int x[4];
_mm256_storeu_si256((__m256i*)x,ymm);
printf("%016lX %016lX %016lX %016lX\n", x[3],x[2],x[1],x[0]);
return 0;
}
int main()
{
uint64_t x;
__m256i nibble_x16 = _mm256_set_epi16(0x000F,0x000E,0x000D,0x000C, 0x000B,0x000A,0x0009,0x0008,
0x0007,0x0006,0x0005,0x0004, 0x0003,0x0002,0x0001,0x0000);
printf("AVX variable: \n");
print_avx2_hex(nibble_x16);
x = hnib2byte(nibble_x16);
printf("With hnib2byte x = %016lX \n\n",x);
printf("AVX variable: \n");
print_avx2_hex(nibble_x16);
x = hnib2byte_v2(nibble_x16);
printf("With hnib2byte_v2 x = %016lX \n",x);
return 0;
}
输出为:
$ ./a.out
AVX variable:
000F000E000D000C 000B000A00090008 0007000600050004 0003000200010000
With hnib2byte x = EFCDAB8967452301
AVX variable:
000F000E000D000C 000B000A00090008 0007000600050004 0003000200010000
With hnib2byte_v2 x = EFCDAB8967452301
两种方法的输出对于此处选择的输入是相等的。
除了应该在循环外加载洗牌常量外,它只编译为五个指令:
vpslld
、vpor
、vpshufb
、vpermd
和vmovq
,比你的解少三个。
给定一个或多个 __m128i
或 __m256i
每个 16 位元素包含一个半字节,将它们组合并打包为每个 8 位元素一个字节的最快方法是什么(即 (hi << 4) | lo
对于相邻的 16 位元素)?
这是我想出的最好的方法,不幸的是,它与标量代码相当:
const static __m256i shufmask = _mm256_setr_epi8(
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255,
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255);
const static __m256i high4 = _mm256_setr_epi8(
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0
);
inline static int64_t hnib2byte(__m256i nibbles) { // (a << 4) | b;
// hi 0 lo 0, ...
__m256i upper = _mm256_slli_epi16(nibbles, 4);
// Align upper and lower halves so they can be ORed vertically
// lo 0 0 0, ...
__m256i lower = _mm256_shuffle_epi8(nibbles, shufmask);
// ab x x x, ...
__m256i or = _mm256_or_si256(upper, lower);
// Pack into bytes
or = _mm256_and_si256(or, high4);
__m256i pack16 = _mm256_packus_epi16(or, or);
const int _3to2 = 0b00001000;
__m256i perm16 = _mm256_permute4x64_epi64(pack16, _3to2); // :(
__m256i pack8 = _mm256_packus_epi16(perm16, perm16);
return _mm_cvtsi128_si64(_mm256_castsi256_si128(pack8));
}
包括 AVX2 在内的指令都是公平的。 AVX-512 中的掩码移位提供了更好的选择。这是在循环中调用的,因此尽早将半字节打包到 8 位元素中也是公平的游戏。
下面的解决方案 hnib2byte_v2
应该比您的解决方案快一点,至少在 Intel 处理器上是这样。
指令 vpermd
或内在 _mm256_permutevar8x32_epi32
在 AMD Ryzen 上运行缓慢。在那个平台上最好用_mm256_extracti128_si256
提取[=16=的高128位通道,用_mm256_castsi256_si128
提取低128位通道,把这两个结合起来_mm256_or_si256
获取最低 64 位的答案。
/*
gcc -O3 -m64 -Wall -mavx2 -march=broadwell nibble2byte.c
*/
#include <immintrin.h>
#include <stdio.h>
#include <stdint.h>
int print_avx2_hex(__m256i ymm);
inline static int64_t hnib2byte_v2(__m256i nibbles) {
__m256i shufmask8 = _mm256_set_epi8(-1,-1,-1,-1, -1,-1,-1,-1, 14,10,6,2, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 14,10,6,2);
__m256i shufmask32 = _mm256_set_epi32(7,7,7,7,7,7,5,0);
__m256i lower = _mm256_slli_epi32(nibbles, 20);
// 00E0000000C00000 00A0000000800000 0060000000400000 0020000000000000
__m256i up_lo = _mm256_or_si256(lower,nibbles);
// 00EF000E00CD000C 00AB000A00890008 0067000600450004 0023000200010000
__m256i pck = _mm256_shuffle_epi8(up_lo,shufmask8);
// 0000000000000000 EFCDAB8900000000 0000000000000000 0000000067452301
__m256i pck64 = _mm256_permutevar8x32_epi32(pck,shufmask32);
// 0000000000000000 0000000000000000 0000000000000000 EFCDAB8967452301
// print_avx2_hex(lower);
// print_avx2_hex(up_lo);
// print_avx2_hex(pck);
// print_avx2_hex(pck64);
return _mm_cvtsi128_si64(_mm256_castsi256_si128(pck64));
}
inline static int64_t hnib2byte(__m256i nibbles) { // (a << 4) | b;
__m256i shufmask = _mm256_setr_epi8(
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255,
2, 255, 255, 255, 6, 255, 255, 255, 10, 255, 255, 255, 14, 255, 255, 255);
__m256i high4 = _mm256_setr_epi8(
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0);
// hi 0 lo 0, ...
__m256i upper = _mm256_slli_epi16(nibbles, 4);
// Align upper and lower halves so they can be ORed vertically
// lo 0 0 0, ...
__m256i lower = _mm256_shuffle_epi8(nibbles, shufmask);
// ab x x x, ...
__m256i or = _mm256_or_si256(upper, lower);
// Pack into bytes
or = _mm256_and_si256(or, high4);
__m256i pack16 = _mm256_packus_epi16(or, or);
const int _3to2 = 0b00001000;
__m256i perm16 = _mm256_permute4x64_epi64(pack16, _3to2); // :(
__m256i pack8 = _mm256_packus_epi16(perm16, perm16);
return _mm_cvtsi128_si64(_mm256_castsi256_si128(pack8));
}
int print_avx2_hex(__m256i ymm)
{
long unsigned int x[4];
_mm256_storeu_si256((__m256i*)x,ymm);
printf("%016lX %016lX %016lX %016lX\n", x[3],x[2],x[1],x[0]);
return 0;
}
int main()
{
uint64_t x;
__m256i nibble_x16 = _mm256_set_epi16(0x000F,0x000E,0x000D,0x000C, 0x000B,0x000A,0x0009,0x0008,
0x0007,0x0006,0x0005,0x0004, 0x0003,0x0002,0x0001,0x0000);
printf("AVX variable: \n");
print_avx2_hex(nibble_x16);
x = hnib2byte(nibble_x16);
printf("With hnib2byte x = %016lX \n\n",x);
printf("AVX variable: \n");
print_avx2_hex(nibble_x16);
x = hnib2byte_v2(nibble_x16);
printf("With hnib2byte_v2 x = %016lX \n",x);
return 0;
}
输出为:
$ ./a.out
AVX variable:
000F000E000D000C 000B000A00090008 0007000600050004 0003000200010000
With hnib2byte x = EFCDAB8967452301
AVX variable:
000F000E000D000C 000B000A00090008 0007000600050004 0003000200010000
With hnib2byte_v2 x = EFCDAB8967452301
两种方法的输出对于此处选择的输入是相等的。
除了应该在循环外加载洗牌常量外,它只编译为五个指令:
vpslld
、vpor
、vpshufb
、vpermd
和vmovq
,比你的解少三个。