SIMD 将 12 位字段解包为 16 位

问题描述 投票:0回答:1

我需要从每个 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 解决这个问题? 可能使用解包/洗牌/排列内在函数?

c avx bit-fields avx2 pixelformat
1个回答
5
投票

我假设您在一个大数组上循环执行此操作。 如果您仅使用

__m128i
加载,则会有 15 个有用字节,这只会在
__m256i
输出中产生 20 个输出字节。 (嗯,我猜输出的第 21 个字节会出现,作为输入向量的第 16 个字节,新位字段的前 8 个字节。但是你的下一个向量将需要以不同的方式进行洗牌。)

最好使用 24 字节的输入,产生 32 字节的输出。 理想情况下,负载会在中间分开,因此低 12 字节位于低 128 位“通道”中,从而避免需要像

_mm256_permutevar8x32_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
变量混合不同)。 尽管如此,这意味着英特尔的后端 ALU 瓶颈最多为每 2 个周期一个向量。 如果你的 src 和 dst 在 L2 缓存(或者可能只是 L1d)中很热,那可能是 the 瓶颈,但这对于前端来说已经是 5 uops,所以有了循环开销和存储,你已经接近前端瓶颈。

与另一个

vpand
/
vpor
混合会花费更多的前端微指令,但会减轻英特尔的后端瓶颈(在 Ice Lake 之前)。 在 AMD 上情况会更糟,其中
vpblendw
可以在 4 个 FP 执行端口中的任何一个上运行,而在 Ice Lake 上情况更糟,其中
vpblendw
可以在 p1 或 p5 上运行。 就像我说的,无论如何,缓存加载/存储吞吐量可能是比端口 5 更大的瓶颈,因此更少的前端微指令肯定更好让乱序执行器看得更远。


这可能不是最佳的;也许有某种方法可以通过更便宜地将偶数(低)和奇数(高)位字段放入两个单独输入向量的底部 8 个字节来设置

vpunpcklwd
? 或者进行设置,以便我们可以与 OR 混合,而不需要使用仅在 Skylake 上的端口 5 上运行的
vpblendw
清除一个输入中的垃圾?

或者我们可以用

vpsrlvd
做什么? (但不是
vpsrlvw
- 那需要 AVX-512)。


如果您有 AVX512VBMI,则

vpmultishiftqb
是并行位域提取。 您只需将正确的 3 字节对混入正确的 64 位 SIMD 元素,然后用一个
_mm256_multishift_epi64_epi8
将好的位放在您想要的位置,然后用
_mm256_and_si256
将每个的高 4 位清零16 位字段就可以了。 (不能完全用 0 掩码处理所有事情,或者将一些零混入多班输入中,因为不会有任何与低 12 位字段连续的内容。)或者您可以设置一个
 srli_epi16
适用于低位和高位,而不需要 AND 常量,通过让多移位位域提取将两个输出字段与您想要的 16 位顶部的位对齐元素。

这也可能允许比字节更大粒度的洗牌,尽管

vpermb
在具有 AVX512VBMI 的 CPU 上实际上很快,不幸的是 Ice Lake 的
vpermw
vpermb
慢。

使用 AVX-512 而不是 AVX512VBMI,在 256 位块中工作可以让我们做与 AVX2 相同的事情,但避免混合。 相反,使用合并掩码进行右移,或使用带有控制向量的

vpsrlvw
来仅移动奇数元素。 对于 256 位向量,这可能与
vpmultishiftqb
一样好。

© www.soinside.com 2019 - 2024. All rights reserved.