在单臂霓虹灯寄存器中有效地将 8 位数字扩展到 12 位

Efficiently extend 8-bit numbers to 12-bits in a single arm neon register

我在霓虹灯寄存器中加载了 4 个字节。我怎样才能有效地将它转换为 12 位,例如我需要在第一个字节后插入 4 个零位,在第二个字节后插入 8 个零位,依此类推。例如,如果我有这 4 个十六进制字节:

01 02 03 04

It would end up with this in hex:

01 20 00 03 40

相同的操作表示为一个简单的 c 函数,该函数对表示 4 个输入字节的 32 位变量进行操作:

uint64_t expand12(uint32_t i)
{
    uint64_t r = (i & 0xFF);
    r |= ((i & 0x0000ff00) << 4); // shift second byte by 4 bits
    r |= ((i & 0x00ff0000) << 8); // shift third byte by 8 bits
    r |= (((uint64_t)(i & 0xff000000)) << 12); // 4th by 12
    return r;
}

所以,如果我在 uint8x8_t neon 寄存器中有这些字节,那么在 neon 中实现相同操作的好方法是什么,以便相同的寄存器将以这些移位值结束?

请注意,如果有任何帮助,所有四个字节的前 4 位都为零。

更新: 在我的例子中,我有 4 个 uint16x8_t 寄存器,对于每个寄存器,我需要计算所有通道的总和 (vaddv_u16),然后对该总和执行 vclz_u16,然后将这四个总和组合成一个霓虹灯寄存器将它们分开 12 位:

uint64_t compute(uint16x8_t a, uint16x8_t b, uint16x8_t c, uint16x8_t d)
{
    u16 a0 = clz(vaddv(a));
    u16 b0 = clz(vaddv(b));
    u16 c0 = clz(vaddv(c));
    u16 d0 = clz(vaddv(d));
    return (a0 << 36) | (b0 << 24) | (c0 << 12) | (d0);
}

注意,这是伪代码,我需要在 neon 寄存器中得到结果。

以防万一,在我的代码中,我有一个函数可以在 4 个 uint16x8_t 寄存器中查找最大元素的索引。在该函数中,这四个寄存器被 vanded 与最大元素复制到所有通道,然后结果被 vorred 与位掩码 {1<<15, 1<<14, ... 1<<0};然后,我将所有通道成对相加,其中的 clz 给出了每个寄存器的最大元素的索引。所有这些我都需要在元素之间插入额外的 4 个零位并存储到霓虹灯寄存器中。 C 中的示例:

void compute(uint16_t *src, uint64_t* dst)
{
    uint64_t x[4];
    for (int i = 0; i < 4; ++i, src+=16)
    {
        int max = 0;
        for (int j = 0; j < 16; ++j)
        {
            if (src[j] > src[max])
                max = j;
        }
        x[i] = max;
    }
    *dst = (x[0] << 36) | (x[1] << 24) | (x[2] << 12) | (x[3]);
}

此函数是大型函数的一部分,该函数在一个循环中执行此计算数百万次,并且使用此函数的结果并且必须在霓虹灯寄存器中。将其视为描述算法的伪代码,如果不清楚这意味着什么:这意味着只有算法很重要,没有需要优化的加载或存储

你必须跳出框框思考。不要拘泥于数据类型和位宽。

uint32_t 只不过是一个包含 4 个 uint8_t 的数组,您可以在加载时轻松地通过 vld4 即时传播。

问题因此变得更易于管理。


void foo(uint32_t *pDst, uint32_t *pSrc, uint32_t length)
{
    length >>= 4;
    int i;
    uint8x16x4_t in, out;
    uint8x16_t temp0, temp1, temp2;

    for (i = 0; i < length; ++i)
    {
        in = vld4q_u8(pSrc);
        pSrc += 16;

        temp0 = in.val[1] << 4;
        temp1 = in.val[3] << 4;
        temp1 += in.val[1] >> 4;

        out.val[0] = in.val[0] | temp0;
        out.val[1] = in.val[2] | temp1;
        out.val[2] = in.val[3] >> 4;
        out.val[3] = vdupq_n_u8(0);

        vst4q_u8(pDst, out);
        pDst += 16;
    }
}

请注意,我省略了剩余交易,如果展开得更深,它会 运行 快得多。

更重要的是,我会毫不犹豫地用汇编语言编写这个函数,因为我认为编译器不会如此巧妙地管理寄存器,以至于 out.val[3] 仅在循环外一次被零初始化.

而且我也怀疑 temp1 += in.val[1] >> 4; 会转换为 vsra,因为指令的非单独目标操作数的性质。谁知道?

编译器糟透了。


更新:好的,这里有满足您需求的代码,用汇编语言编写,适用于两种架构。


aarch32

vtrn.16     q0, q1
vtrn.16     q2, q3
vtrn.32     q0, q2
vtrn.32     q1, q3

vadd.u16    q0, q1, q0
vadd.u16    q2, q3, q2

adr     r12, shift_table

vadd.u16    q0, q2, q0

vld1.64     {q3}, [r12]


vadd.u16    d0, d1, d0
vclz.u16    d0, d0          // d0 contains the leading zeros

vmovl.u16   q0, d0

vshl.u32    q1, q0, q3

vpadal.u32  d3, d2          // d3 contains the final result


.balign 8
shift_table:
    .dc.b   0x00, 0x00, 0x00, 0x00,     0x0c, 0x00, 0x00, 0x00,     0x18, 0x00, 0x00, 0x00,     0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4

aarch64

trn1        v16.8h, v0.8h, v1.8h
trn1        v18.8h, v2.8h, v3.8h
trn2        v17.8h, v0.8h, v1.8h
trn2        v19.8h, v2.8h, v3.8h

trn2        v0.4s, v18.4s, v16.4s
trn1        v1.4s, v18.4s, v16.4s
trn2        v2.4s, v19.4s, v17.4s
trn1        v3.4s, v19.4s, v17.4s

add         v0.8h, v1.8h, v0.8h
add         v2.8h, v3.8h, v2.8h

adr     x16, shift_table

add         v0.8h, v2.8h, v0.8h

ld1         {v3.2d}, [x16]

mov         v1.d[0], v0.d[1]

add         v0.4h, v1.4h, v0.4h

clz         v0.4h, v0.4h                // v0 contains the leading zeros

uxtl        v0.4s, v0.4h

ushl        v0.4s, v0.4s, v3.4s

mov         v1.d[0], v0.d[1]

uadalp      v1.1d, v0.2s                // v1 contains the final result


.balign 8
shift_table:
.dc.b   0x00, 0x00, 0x00, 0x00,     0x0c, 0x00, 0x00, 0x00,     0x18, 0x00, 0x00, 0x00,     0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4

** 您可能需要在 Clang

中将 .dc.b 更改为 .byte