【问题标题】:SIMD unpack 12-bit fields to 16-bitSIMD 将 12 位字段解压缩为 16 位
【发布时间】:2021-02-24 09:20:22
【问题描述】:

我需要从每个 24 位输入中解压缩两个 16 位值。 (3 个字节 -> 4 个字节)。我已经以天真的方式做到了,但我对性能不满意。

例如,InBuffer 是__m128i:

value1 = (uint16_t)InBuffer[0:11]        // bit-ranges
value2 = (uint16_t)InBuffer[12:24]

value3 = (uint16_t)InBuffer[25:36] 
value4 = (uint16_t)InBuffer[37:48]
... for all the 128 bits.

解包后,值应存储在__m256i变量中。

如何使用 AVX2 解决这个问题?可能使用 unpack / shuffle / permute 内在函数?

【问题讨论】:

  • 您是否寻找过现有的 12->16 位转换函数?我希望这是人们已经针对视频像素格式转换(例如平面 yuv12 到未打包的 16 位格式,每个 uint16_t 一个样本)进行了优化的东西。例如Python: Fast way to read/unpack 12 bit little endian packed data 有一个 numpy 的答案;如果您的项目许可证与 NumPy 的开源许可证兼容,您可以去那里寻找。 (虽然 IDK 即使使用 AVX2。)
  • 您是否在一个大型数组上循环执行此操作,并将结果存储到内存中? (使用 32 字节存储,由重叠 8 个字节的 32 字节加载馈送?或者使用 __m128i 加载,您将 15 个字节转换为 20,并将存储重叠 12 个字节?)未对齐的 __m256i 加载每个保留 12 个有用字节128-bit half 可能是你最好的选择,所以你不需要任何交叉洗牌。只是_mm256_shuffle_epi8 和一些转变/和。
  • 是的,我想在一个大数组(视频帧)上进行。根据您的回答,代码应该是什么样子?

标签: c avx bit-fields avx2 pixelformat


【解决方案1】:

我假设您在一个大型数组的循环中执行此操作。如果您只使用__m128i 加载,那么您将有 15 个有用字节,这只会在您的__m256i 输出中产生 20 个输出字节。 (好吧,我猜输出的第 21 个字节会出现,作为输入向量的第 16 个字节,新位域的前 8 个字节。但是你的下一个向量需要以不同的方式洗牌。)

最好使用 24 字节的输入,产生 32 字节的输出。理想情况下,负载会从中间分开,因此低 12 字节位于低 128 位“通道”中,从而避免需要像 _mm256_permutexvar_epi32 这样的通道交叉洗牌。相反,您可以 _mm256_shuffle_epi8 将字节放在您想要的位置,设置一些移位/和。

// uses 24 bytes starting at p by doing a 32-byte load from p-4.
// Don't use this for the first vector of a page-aligned array, or the last
inline
__m256i unpack12to16(const char *p)
{
    __m256i v = _mm256_loadu_si256( (const __m256i*)(p-4) );
   // v= [ x H G F E | D C B A x ]   where each letter is a 3-byte pair of two 12-bit fields, and x is 4 bytes of garbage we load but ignore

    const __m256i bytegrouping =
        _mm256_setr_epi8(4,5, 5,6,  7,8, 8,9,  10,11, 11,12,  13,14, 14,15, // low half uses last 12B
                         0,1, 1,2,  3,4, 4,5,   6, 7,  7, 8,   9,10, 10,11); // high half uses first 12B
    v = _mm256_shuffle_epi8(v, bytegrouping);
    // each 16-bit chunk has the bits it needs, but not in the right position

    // in each chunk of 8 nibbles (4 bytes): [ f e d c | d c b a ]
    __m256i hi = _mm256_srli_epi16(v, 4);                              // [ 0 f e d | xxxx ]
    __m256i lo  = _mm256_and_si256(v, _mm256_set1_epi32(0x00000FFF));  // [ 0000 | 0 c b a ]

    return _mm256_blend_epi16(lo, hi, 0b10101010);
      // nibbles in each pair of epi16: [ 0 f e d | 0 c b a ] 
}

// Untested: I *think* I got my shuffle and blend controls right, but didn't check.

它像这样编译 (Godbolt) 和 clang -O3 -march=znver2。当然,内联版本会在循环外加载一次向量常量。

unpack12to16(char const*):                    # @unpack12to16(char const*)
        vmovdqu ymm0, ymmword ptr [rdi - 4]
        vpshufb ymm0, ymm0, ymmword ptr [rip + .LCPI0_0] # ymm0 = ymm0[4,5,5,6,7,8,8,9,10,11,11,12,13,14,14,15,16,17,17,18,19,20,20,21,22,23,23,24,25,26,26,27]
        vpsrlw  ymm1, ymm0, 4
        vpand   ymm0, ymm0, ymmword ptr [rip + .LCPI0_1]
        vpblendw        ymm0, ymm0, ymm1, 170           # ymm0 = ymm0[0],ymm1[1],ymm0[2],ymm1[3],ymm0[4],ymm1[5],ymm0[6],ymm1[7],ymm0[8],ymm1[9],ymm0[10],ymm1[11],ymm0[12],ymm1[13],ymm0[14],ymm1[15]
        ret

在 Intel CPU(Ice Lake 之前)上,vpblendw 仅在端口 5 (https://uops.info/) 上运行,与 vpshufb (...shuffle_epi8) 竞争。但它是一个带有即时控制的单一微指令(不像vpblendvb 变量混合)。尽管如此,这意味着在英特尔上每 2 个周期最多一个向量的后端 ALU 瓶颈。如果您的 src 和 dst 在 L2 缓存中很热(或者可能只有 L1d),那可能是 瓶颈,但这已经是前端的 5 微指令,所以有了循环开销和存储已经接近前端瓶颈。

与另一个 vpand / vpor 混合会花费更多的前端 uops,但会缓解英特尔的后端瓶颈(在 Ice Lake 之前)。在 AMD 上会更糟,vpblendw 可以在 4 个 FP 执行端口中的任何一个上运行,而在 Ice Lake 上更糟糕,vpblendw 可以在 p1 或 p5 上运行。就像我说的,缓存加载/存储吞吐量可能是比端口 5 更大的瓶颈,所以更少的前端 uops 肯定会更好地让乱序的 exec 看得更远。


这可能不是最优的;也许有一些方法可以通过将偶数(低)和奇数(高)位字段更便宜地放入两个单独输入向量的底部 8 字节中来设置vpunpcklwd?或者进行设置,以便我们可以与 OR 混合,而无需使用仅在 Skylake 的端口 5 上运行的 vpblendw 清除一个输入中的垃圾?

或者我们可以用vpsrlvd 做些什么? (但不是 vpsrlvw - 这需要 AVX-512)。


如果您有 AVX512VBMIvpmultishiftqb is a parallel bitfield-extract。您只需要将正确的 3 字节对洗牌到正确的 64 位 SIMD 元素中,然后一个 _mm256_multishift_epi64_epi8 将好的位放在您想要的位置,_mm256_and_si256 将每个的高 4 位归零16 位字段可以解决问题。 (不能完全用 0 掩码处理所有事情,或者将一些零改组到多移位的输入中,因为不会有任何与低 12 位字段连续的内容。)或者你可以只设置一个 @ 987654346@ 既适用于低位也适用于高位,而不需要 AND 常数,方法是让多移位位域提取将两个输出域与 16 位元素顶部的所需位对齐。

这也可能允许粒度大于字节的随机播放,尽管 vpermb 实际上在具有 AVX512VBMI 的 CPU 上速度很快,不幸的是 Ice Lake 的 vpermwvpermb 慢。

使用 AVX-512 而不是 AVX512VBMI,在 256 位块中工作可以让我们做与 AVX2 相同的事情,但避免混合。相反,使用合并掩码进行右移,或使用vpsrlvw 和控制向量仅移动奇数元素。对于 256 位向量,这可能与 vpmultishiftqb 一样好。

【讨论】:

  • 感谢您的详细解释。它有效,除了一件事 - 我需要交换公共(字节分组之前)字节的半字节以获得正确的结果。我该如何解决这个问题?
  • 据我了解,我需要为每个第 2 个字节创建一个掩码 - 'const __m256i vmask = _mm_set1_epi16(0x00ffff00)',而不是 't = _mm_and_si256(v, vmask);'洗牌后。现在我需要以某种方式交换“字节顺序”和“合并”这两个变量。对吗?
  • @OC87:“公共”是指中间字节,它包含来自 2 个单独字段的位?您的幼稚/标量 C 实现是否也需要该修复?也许在问题中显示它以明确哪些位/字节来自哪里。因为这听起来很奇怪。在 SIMD 中,希望 _mm256_shuffle_epi8 在某些时候可以交换保存 4 位部分的字节对,或重新设计移位。 (也可能对 AND 结果进行转换,而不是并行进行)。
  • @OC87:注意_mm_set1_epi16(0x00ffff00) 没有意义;该常数不适合 16 位。也许你的意思是 set1_epi32。但是在bytegrouping shuffle 之后交换半字节可能更容易,因此您可以使用 32 位重复模式而不是 24 位重复。
  • 彼得,你是对的。我不需要交换中间字节。谢谢!
猜你喜欢
  • 2014-08-05
  • 1970-01-01
  • 2019-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-19
相关资源
最近更新 更多