将最佳 uint8_t 位图转换为 8 x 32 位 SIMD“布尔”向量

Posted

技术标签:

【中文标题】将最佳 uint8_t 位图转换为 8 x 32 位 SIMD“布尔”向量【英文标题】:Optimal uint8_t bitmap into a 8 x 32bit SIMD "bool" vector 【发布时间】:2015-02-23 21:35:51 【问题描述】:

作为压缩算法的一部分,我正在寻找实现以下目标的最佳方法:

我在uint8_t 中有一个简单的位图。例如 01010011

我想要的是__m256i 的形式:(0, maxint, 0, maxint, 0, 0, maxint, maxint)

实现此目的的一种方法是将一个 8 x maxint 的向量改组为一个零向量。但这首先需要我将我的uint8_t 扩展为正确的随机播放位图。

我想知道是否有更好的方法?

【问题讨论】:

想不出一个好的解决方案。您可以使用由 uint8_t 索引的所有预先计算的 _m256i 创建一个表。由于混合说明需要立即执行,因此您可以有一个混合表。我认为 AVX512 会对此有所帮助。 或者,您可以尝试将字节广播到每个通道中,屏蔽每个通道中的单个有效位,最后进行比较以创建掩码。 @MarcGlisse 哈哈,我们都在等待 AVX512。这实际上是 2 条指令。 kmov + vmovdqa32 @doynax,是的,这就是我想出的解决方案。 继续拼图,这是我的相关问题:***.com/questions/28735461/… 【参考方案1】:

我想我最初可能会选择“蛮力和无知”的方法,可能是这样的:

uint8_t u = 0x53; // 01010011

const union 
    uint32_t a[4];
    __m128i v;
 kLUT[16] =     0,  0,  0,  0  ,
                 -1,  0,  0,  0  ,
                  0, -1,  0,  0  ,
                 -1, -1,  0,  0  ,
                  0,  0, -1,  0  ,
                 -1,  0, -1,  0  ,
                  0, -1, -1,  0  ,
                 -1, -1, -1,  0  ,
                  0,  0,  0, -1  ,
                 -1,  0,  0, -1  ,
                  0, -1,  0, -1  ,
                 -1, -1,  0, -1  ,
                  0,  0, -1, -1  ,
                 -1,  0, -1, -1  ,
                  0, -1, -1, -1  ,
                 -1, -1, -1, -1   ;
__m256i v = _mm256_set_m128i(kLUT[u >> 4].v, kLUT[u & 15].v);

使用clang -O3 编译为:

movl    %ebx, %eax                ;; eax = ebx = u
andl    $15, %eax                 ;; get low offset = (u & 15) * 16
shlq    $4, %rax
leaq    _main.kLUT(%rip), %rcx    ;; rcx = kLUT
vmovaps (%rax,%rcx), %xmm0        ;; load low half of ymm0 from kLUT
andl    $240, %ebx                ;; get high offset = (u >> 4) * 16
vinsertf128 $1, (%rbx,%rcx), %ymm0, %ymm0
                                  ;; load high half of ymm0 from kLUT

FWIW 我为三个实现组合了一个简单的测试工具:(i) 一个简单的标量代码参考实现,(ii) 上述代码,(iii) 基于@Zboson 答案的实现,(iv) 略微改进的版本(iii)和(v)使用@MarcGlisse的建议对(iv)的进一步改进。我使用 2.6GHz Haswell CPU(使用 clang -O3 编译)得到以下结果:

scalar code:                                 7.55336 ns / vector
Paul R:                                      1.36016 ns / vector
Z boson:                                     1.24863 ns / vector
Z boson (improved):                          1.07590 ns / vector
Z boson (improved + @MarcGlisse suggestion): 1.08195 ns / vector

所以@Zboson 的解决方案赢了大约 10% - 20%,大概是因为他们只需要 1 个负载,而我的需要 2 个。

如果我们得到任何其他实现,我会将它们添加到测试工具中并更新结果。


@Zboson 实现的略微改进版本:
__m256i v = _mm256_set1_epi8(u);
v = _mm256_and_si256(v, mask);
v = _mm256_xor_si256(v, mask);
return _mm256_cmpeq_epi32(v, _mm256_setzero_si256());


@Zboson 实施的进一步改进版本,结合了@MarcGlisse 的建议:
__m256i v = _mm256_set1_epi8(u);
v = _mm256_and_si256(v, mask);
return _mm256_cmpeq_epi32(v, mask);

(注意mask需要在每个32位元素中包含复制的8位值,即0x01010101, 0x02020202, ..., 0x80808080


【讨论】:

是的,你可能是对的 - Haswell/Broadwell 上未对齐负载的惩罚非常小,但如果可能的话,最好保持对齐。我只是把上面的例子作为一个起点而不是一个实际的解决方案,但我会努力改进它。 我刚刚检查过,似乎要初始化我的数组,最好的选择是const __m128i tab[]=_mm_set_epi32(0,0,0,0),...,并希望在编译时评估 _mm_set_epi32 数组不必动态初始化。所以使用标量数组(你在做什么)是有意义的。 另外一个用于改进我的答案,测试性能,并向我展示你使用 LUT 的聪明解决方案(即使你称之为蛮力,它对我来说仍然很聪明)。 是的,我认为您的蛮力技术在其他情况下也很有用。我用你的改进更新了我的答案(当然是给你的功劳)。 即使在我的 Haswell CPU 上它不是更快,我认为 MarcGlisse 进一步建议的解决方案可能是可行的方法 - 它使用更少的指令,可能只是我的测试工具是 I/ O bound,因此它在其他系统或其他上下文中可能更快。【参考方案2】:

这是一个基于此问题fastest-way-to-broadcast-32-bits-in-32-bytes 的变体的解决方案(PaulR 改进了我的解决方案,请参阅我的答案或他的答案的结尾)。

__m256i t1 = _mm256_set1_epi8(x);
__m256i t2 = _mm256_and_si256(t1, mask);
__m256i t4 = _mm256_cmpeq_epi32(t2, _mm256_setzero_si256());
t4 = _mm256_xor_si256(t4, _mm256_set1_epi32(-1));

我现在没有 AVX2 硬件来测试它,但这里有一个 SSE2 版本,显示它可以工作,它还显示了如何定义掩码。

#include <x86intrin.h>
#include <stdint.h>
#include <stdio.h>

int main(void) 
    char mask[32] = 
        0x01, 0x00, 0x00, 0x00,
        0x02, 0x00, 0x00, 0x00,
        0x04, 0x00, 0x00, 0x00,
        0x08, 0x00, 0x00, 0x00,
        0x10, 0x00, 0x00, 0x00,
        0x20, 0x00, 0x00, 0x00,
        0x40, 0x00, 0x00, 0x00,
        0x80, 0x00, 0x00, 0x00,
    ;
    __m128i mask1 = _mm_loadu_si128((__m128i*)&mask[ 0]);
    __m128i mask2 = _mm_loadu_si128((__m128i*)&mask[16]);

    uint8_t x = 0x53; //0101 0011
    __m128i t1 = _mm_set1_epi8(x);
    __m128i t2 = _mm_and_si128(t1, mask1);
    __m128i t3 = _mm_and_si128(t1, mask2);
    __m128i t4 = _mm_cmpeq_epi32(t2,_mm_setzero_si128());
    __m128i t5 = _mm_cmpeq_epi32(t3,_mm_setzero_si128());
    t4 = _mm_xor_si128(t4, _mm_set1_epi32(-1));
    t5 = _mm_xor_si128(t5, _mm_set1_epi32(-1));

    int o1[4], o2[4];
    _mm_store_si128((__m128i*)o1, t4);
    _mm_store_si128((__m128i*)o2, t5);
    for(int i=0; i<4; i++) printf("%d \n", o1[i]);
    for(int i=0; i<4; i++) printf("%d \n", o2[i]);


编辑:

PaulR 改进了我的解决方案

__m256i v = _mm256_set1_epi8(u);
v = _mm256_and_si256(v, mask);
v = _mm256_xor_si256(v, mask);
return _mm256_cmpeq_epi32(v, _mm256_setzero_si256());

掩码定义为

int mask[8] = 
    0x01010101, 0x02020202, 0x04040404, 0x08080808,
    0x10101010, 0x20202020, 0x40404040, 0x80808080,
;

有关更多详细信息,请参阅他的性能测试答案。

【讨论】:

这是结果:-1 -1 0 0 -1 0 -1 0,如果使用无符号整数并将其反转,我想这是预期的输出。 您不能测试t1&amp;mask==mask 而不是t1&amp;mask!=0 来保存异或吗? @MarcGlisse:我刚刚意识到,如果您更改掩码,那么您仍然可以这样做 - 奇怪的是,虽然没有 XOR 的较短版本并没有更快。 @MarcGlisse,很好的建议t1&amp;mask==mask。我早该想到的。 @ThomasKejser,是的,看起来是这样。我收到g++ 的警告,除非我使用未签名(我应该更频繁地使用-Wall)。【参考方案3】:

根据所有答案,我使用 Agner Fog 的优秀库(该库以通用抽象处理 AVX2、AVX 和 SSE 解决方案)破解了一个解决方案。想我会分享它作为替代答案:

// Used to generate 32 bit vector bitmasks from 8 bit ints
static const Vec8ui VecBitMask8(
      0x01010101
    , 0x02020202
    , 0x04040404
    , 0x08080808
    , 0x10101010
    , 0x20202020
    , 0x40404040
    , 0x80808080);

// As above, but for 64 bit vectors and 4 bit ints
static const Vec4uq VecBitMask4(
      0x0101010101010101
    , 0x0202020202020202
    , 0x0404040404040404
    , 0x0808080808080808);

template <typename V>
inline static Vec32c getBitmapMask();

template <> inline Vec32c getBitmapMask<Vec8ui>() return VecBitMask8;;
template <> inline Vec32c getBitmapMask<Vec8i>() return VecBitMask8;;
template <> inline Vec32c getBitmapMask<Vec4uq>() return VecBitMask4;;
template <> inline Vec32c getBitmapMask<Vec4q>() return VecBitMask4;;

// Returns a bool vector representing the bitmask passed.
template <typename V>
static inline V getBitmap(const uint8_t bitMask) 
    Vec32c mask = getBitmapMask<V>();
    Vec32c v1(bitMask);
    v1 = v1 & mask;
    return ((V)v1 == (V)mask);

【讨论】:

酷 - 我试图将它合并到测试工具中,但它会在 clang++ 中引发很多编译错误 - 除了#include &lt;vectorclass.h&gt; 之外,我还需要做任何其他事情来完成这项工作吗? vectorclass.h 应该这样做。你需要用 C++11 编译。 嗯 - 即使使用 -std=c++11 仍然会出现很多错误 - 第一个是:vectorf128.h:215:22: error: ambiguous conversion for functional-style cast from 'const Vec4fb' to 'Vec4ib' - 如果有机会(可能明天),我会尝试不同的编译器。跨度> 我是 Agner 的 VCL 的忠实粉丝。不过,您的代码不是 AVX512 的最佳选择。然而,VCL 没有Vec64c。我认为这是因为 AVX512 仅支持 32 位和 64 位整数。但在你的情况下,你只需要广播字节。之后,您对 32 位整数进行操作。您应该能够调整您的代码,使其也适用于 AVX512。 @Zboson:正确,我目前只编译 256 位寄存器。我最终将需要针对 512 进行调整。此外,在某些时候我可能需要一个 16 位的 int 变体......有很多东西可以添加到 Agner Fog 的库中。一旦我的代码运行起来,我希望能贡献一些东西。

以上是关于将最佳 uint8_t 位图转换为 8 x 32 位 SIMD“布尔”向量的主要内容,如果未能解决你的问题,请参考以下文章

从 uint8_t* 到 uint32_t 的无效转换 - 从 32 位架构迁移到 64 位架构时?

将 unsigned char 数组转换为 uint8_t 数组?

如何将字符数组转换为 uint8_t

uint8_t uint32_t 类型强制转换出错 以及 unsigned char 类型和 unsigned int 类型相互转化

将 std::vector<uint8_t> 转换为 QImage

C ++将mac id字符串转换为uint8_t数组