如何有效地重新排序__m256i向量的字节(将int32_t转换为uint8_t)?

时间:2019-04-24 22:26:05

标签: c++ vectorization simd intrinsics avx2

我需要优化以下压缩操作(在具有AVX2指令的服务器上):

获取浮点数数组的指数,移位并存储到uint8_t数组

我经验不足,建议从https://github.com/feltor-dev/vcl库开始

现在知道了

uint8_t* uin8_t_ptr = ...;
float* float_ptr = ...;
float* final_ptr = float_ptr + offset;

for (; float_ptr < final_ptr; float_ptr+=8) {
    Vec8f vec_f = Vec8f().load(float_ptr);
    Vec8i vec_i = fraction(vec_f) + 128; // range: 0~255
    ...
}

我的问题是如何有效地将vec_i结果存储到uint8_t数组?

我无法在vcl库中找到相关功能,并且由于我可以访问__m256i数据而试图探索内部指令。

我目前的理解是使用_mm256_shuffle_epi8之类的东西,但不知道有效地做到这一点的最佳方法。

我想知道是否每次尝试充分利用这些位并存储32个元素(使用带有float_ptr + = 32的循环)是

欢迎提出任何建议。谢谢。

1 个答案:

答案 0 :(得分:2)

可能最好的矢量化方法是将vpackssdw / vpackuswbvpermd用作车道内装填后的车道固定装置。

  • _mm256_srli_epi32 将每个32位元素的指数(和符号位)移至底部。不管符号位如何,逻辑移位都会留下非负结果。
  • 然后使用 _mm256_packs_epi32 (带符号输入,带符号饱和输出)将向量对压缩至16位。
  • 然后屏蔽符号位,剩下一个8位指数。我们一直等到现在,所以每条指令可以处理16x uint16_t个元素,而不是8x uint32_t。现在,您拥有16位元素,这些元素的值适合uint8_t而不会溢出。
  • 然后使用 _mm256_packus_epi16 (有符号输入,输出为 unsigned 饱和)将向量对压缩至8位。这实际上很重要,packs会裁剪一些有效值,因为您的数据使用了整个uint8_t范围。
  • VPERMD 可以洗改该向量的8个32位块,这些块来自4x 256位输入向量的每个通道。与How to convert 32-bit float to 8-bit signed char?中的__m256i lanefix = _mm256_permutevar8x32_epi32(abcd, _mm256_setr_epi32(0,4, 1,5, 2,6, 3,7));完全相同,在使用FP-> int转换而不是右移以获取指数字段之后,进行了相同的打包。

每个结果向量具有4倍的load + shift(vpsrld ymm,[mem]),2倍的vpackssdw随机播放,2倍的vpand掩码,1倍的vpackuswb和1倍的{{1 }}。那是4次改组,因此我们希望英特尔HSW / SKL上最好的结果是每4个时钟1个结果向量。 (Ryzen的洗牌吞吐量更好,除了vpermd昂贵之外)。

但这应该是可以实现的,因此平均每个时钟32个输入字节/ 8个输出字节。

总共有10个矢量ALU运算符(包括微融合负载+ ALU),并且应该能够在该时间内执行1个存储。在前端变得比混洗更严重的瓶颈之前,我们有16个总共ouop的空间,包括循环开销。

update:哎呀,我忘了数不偏指数。这将需要额外的vpermd。但是您可以在压缩到8位后执行此操作。(并将其优化为XOR)。我认为我们无法对其进行优化或将其优化,例如掩盖符号位。

使用AVX512BW,您可以对无偏进行字节粒度add,并通过零掩码将每对的高字节清零。这样会将无偏向折叠到16位掩码中。


AVX512F也具有vpmovdb 32-> 8位截断(不饱和),但仅适用于单个输入。因此,您将从一个输入256或512位向量中获得一个64位或128位结果,其中每个输入1个混洗+ 1个添加,而不是2 + 1个混洗+每个输入2个零掩码vpaddb向量。 (两者都需要每个输入向量右移,以将8位指数字段与dword底部的字节边界对齐)

使用 AVX512VBMI,vpermt2b 将允许我们从2个输入向量中获取字节。但是在CannonLake上它的价格为2微秒,因此仅在假设未来的CPU便宜时才有用。它们可以是dword的最高字节,因此我们可以从vpaddb到其自身的向量开始,向左移1。但是最好向左移,因为EVEX编码为{{3 }}或vpaddd 可以从内存中获取具有立即移位计数的数据,这与VEX编码不同。因此,希望我们得到一个微融合的load + shift uop,以节省前端带宽。


另一种选择是移位+混合,导致字节交错的结果修复起来更加昂贵,除非您不介意该顺序。

并且字节粒度混合(不带AVX512BW)需要vpsrld,它为2 oups。 (而且在Haswell上仅在端口5上运行,因此可能是一个巨大的瓶颈。在SKL上,任何矢量ALU端口的价格均为2微秒。)