首页 > 解决方案 > 如何有效地重新排序 __m256i 向量的字节(将 int32_t 转换为 uint8_t)?

问题描述

我需要优化以下压缩操作(在具有可用 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 的循环)将是可行的方法。

欢迎任何建议。谢谢。

标签: c++vectorizationsimdintrinsicsavx2

解决方案


可能你最好的矢量化选择可能是使用vpackssdw/ vpackuswb,并且vpermd作为车道内包后的车道交叉修复。

  • _mm256_srli_epi32将指数(和符号位)移到每个 32 位元素的底部。无论符号位如何,逻辑移位都会留下非负结果。
  • _mm256_packs_epi32然后使用(有符号输入,有符号输出饱和)将向量对打包到 16 位。
  • 然后屏蔽掉符号位,留下一个 8 位指数。我们一直等到现在,所以我们可以在uint16_t每条指令中执行 16x 个元素,而不是 8x uint32_t。现在,您有 16 位元素,其中包含适合uint8_t而不会溢出的值。
  • _mm256_packus_epi16然后用(有符号输入,无符号输出饱和)将向量对压缩到 8 位。这实际上很重要,packs会裁剪一些有效值,因为您的数据使用uint8_t.
  • VPERMD将来自 4x 256 位输入向量的每个通道的该向量的八个 32 位块打乱。与如何将 32 位浮点数转换为 8 位有符号字符中的完全一样的__m256i lanefix = _mm256_permutevar8x32_epi32(abcd, _mm256_setr_epi32(0,4, 1,5, 2,6, 3,7));洗牌?,它在使用 FP->int 转换而不是右移来获取指数字段后执行相同的打包。

对于每个结果向量,您有 4x load+shift(vpsrld ymm,[mem]希望如此)、2x vpackssdwshuffle、2x vpandmask、1xvpackuswb和 1x vpermd。那是 4 次洗牌,所以我们在英特尔 HSW/SKL 上所能期望的最好结果是每 4 个时钟有 1 个结果向量。(Ryzen 具有更好的 shuffle 吞吐量,但vpermd价格昂贵。)

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

总共 10 个向量 ALU 微指令(包括微融合加载+ALU),以及 1 个存储应该能够在那个时间内执行。在前端成为比 shuffle 更严重的瓶颈之前,我们总共有 16 个 uops 的空间,包括循环开销。

更新:哎呀,我忘了计算无偏指数;那将需要额外的add. 但是您可以在打包到 8 位后执行此操作。 (并将其优化为 XOR)。我不认为我们可以将其优化掉或优化成其他东西,比如屏蔽掉符号位。

使用 AVX512BW,您可以对无偏进行字节粒度vpaddb,使用零掩码将每对的高字节归零。这会将无偏折叠到 16 位掩码中。


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

使用AVX512VBMI,vpermt2b我们可以从 2 个输入向量中获取字节。但它在 CannonLake 上的成本为 2 微秒,因此只有在假设的未来 CPU 变得更便宜时才有用。它们可以是 dword 的最高字节,因此我们可以从vpaddd自身的向量开始左移 1。但我们可能最好使用左移,因为 EVEX 编码vpslldorvpsrld 可以从内存中获取数据与 VEX 编码不同,立即移位计数。所以希望我们得到一个微融合的负载+移位uop来节省前端带宽。


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

字节粒度混合(没有 AVX512BW)需要vpblendvb2 微秒。(并且在 Haswell 上仅在端口 5 上运行,因此可能是一个巨大的瓶颈。在 SKL 上,任何矢量 ALU 端口都是 2 微秒。)


推荐阅读