【问题标题】:How do I efficiently reorder bytes of a __m256i vector (convert int32_t to uint8_t)?如何有效地重新排序 __m256i 向量的字节(将 int32_t 转换为 uint8_t)?
【发布时间】:2019-09-14 07:14:01
【问题描述】:

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

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

我没有什么经验,建议从https://github.com/feltor-dev/vcl library开始

现在我有

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 数组中?

由于我可以访问 __m256i 数据,我在 vcl 库中找不到相关函数,并试图探索内在指令。

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

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

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

【问题讨论】:

  • 如果你问得好,你的编译器能有效地为你向量化吗?如果是这样,我会从这里开始,因为它会教你各种技巧,甚至可能做得足够好,你不需要自己做。
  • 感谢您的评论@michael-dorgan,不过我不知道如何继续。你可以分享一些参考/教程吗?我正在使用 gcc 5.4.0。

标签: c++ vectorization simd intrinsics avx2


【解决方案1】:

可能你最好使用vpackssdw / vpackuswbvpermd 作为车道内打包后的车道交叉修复。

  • _mm256_srli_epi32 将指数(和符号位)移到每个 32 位元素的底部。无论符号位如何,逻辑移位都会留下非负结果。
  • 然后使用 _mm256_packs_epi32(有符号输入,有符号输出饱和)将向量对压缩到 16 位。
  • 然后屏蔽掉符号位,留下一个 8 位指数。我们一直等到现在,所以我们可以在每条指令中执行 16x uint16_t 元素,而不是 8x uint32_t。现在,您拥有的 16 位元素的值符合 uint8_t 且不会溢出。
  • 然后使用 _mm256_packus_epi16(有符号输入,无符号输出饱和)将向量对压缩到 8 位。这实际上很重要,packs 会裁剪一些有效值,因为您的数据使用了uint8_t 的全部范围。
  • VPERMD 将来自 4x 256 位输入向量的每个通道的该向量的八个 32 位块打乱。与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)); shuffle,在使用FP->int 转换而不是右移来获取指数字段后执行相同的打包。

每个结果向量,您有 4x 加载+移位(希望是vpsrld ymm,[mem])、2x vpackssdw shuffles、2x vpand 掩码、1x vpackuswb 和 1x vpermd。那是 4 次洗牌,所以我们在英特尔 HSW/SKL 上所能期望的最好结果是每 4 个时钟有 1 个结果向量。 (Ryzen 具有更好的 shuffle 吞吐量,但 vpermd 比较昂贵。)

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

总共10个vector ALU uops(包括micro-fused load+ALU),1个store应该能在那个时候执行。在前端成为比 shuffle 更严重的瓶颈之前,我们有总共 16 个 uops 的空间,包括循环开销。

更新:哎呀,我忘了计算无偏指数;这将需要一个额外的add。但是你可以在打包到 8 位之后这样做。(并将其优化为 XOR)。我不认为我们可以将其优化掉或优化成其他东西,比如屏蔽掉符号位。

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


AVX512F 还具有vpmovdb 32->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 的向量。但我们可能最好使用左移,因为 vpslld 或 @ 的 EVEX 编码987654350@ 可以从内存中获取数据并立即移位计数,这与 VEX 编码不同。所以希望我们能得到一个微融合的负载+移位uop来节省前端带宽。


另一种选择是移位 + 混合,导致修复成本更高的字节交错结果,除非您不介意这种顺序。

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

【讨论】:

  • 谢谢你,彼得。我需要消化一下,因为我刚刚发现了这个包含内在指令的新世界。
猜你喜欢
  • 2019-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-23
  • 2019-11-21
  • 2012-06-07
  • 1970-01-01
相关资源
最近更新 更多