可能你最好使用vpackssdw / vpackuswb 和vpermd 作为车道内打包后的车道交叉修复。
-
_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 微秒。)