C++ 高效解包并重新整理手臂上的8条短裤

C++ 高效解包并重新整理手臂上的8条短裤,c++,arm,intrinsics,neon,C++,Arm,Intrinsics,Neon,假设我有这个输入数组: uint16_t input[] = { 1, 2, 3, 4, 11, 22, 33, 44 }; 如何从该输入中获得具有以下内容的四个q向量: uint16x8_t q1 = { 11, 11, 1, 1, 11, 11, 1, 1 }; uint16x8_t q2 = { 22, 22, 2, 2, 22, 22, 2, 2 }; uint16x8_t q3 = { 33, 33, 3, 3, 33, 33, 3, 3 }; uint16x8_t q4 = { 4

假设我有这个输入数组:

uint16_t input[] = { 1, 2, 3, 4, 11, 22, 33, 44 };
如何从该输入中获得具有以下内容的四个q向量:

uint16x8_t q1 = { 11, 11, 1, 1, 11, 11, 1, 1 };
uint16x8_t q2 = { 22, 22, 2, 2, 22, 22, 2, 2 };
uint16x8_t q3 = { 33, 33, 3, 3, 33, 33, 3, 3 };
uint16x8_t q4 = { 44, 44, 4, 4, 44, 44, 4, 4 };
最好使用霓虹灯内部元件

似乎可以产生最佳的arm64,但armv7似乎不是最佳的。有什么可以改进的或者完全不同的方法是可能的


请注意,我的示例代码将q1、q2、q3输出到一些内存中,在实际代码中,我需要先计算q1、q2、q3,然后才能使用它们。

您的函数太复杂了

  • 您应该避免使用
    vcombine
    ,因为它通常会导致多个 不必要的
    vmov
    等价物
  • 您应该更喜欢
    vtrn
    而不是
    vzip/vuzp
    ,因为
    vtrn
    要快得多

aarch32
aarch64

ld2     {v2.4h, v3.4h}, [input], #16

rev64   v2.4h, v2.4h
rev64   v3.4h, v3.4h
trn2    v0.4h, v2.4h, v2.4h
trn2    v1.4h, v3.4h, v3.4h
trn1    v2.4h, v2.4h, v2.4h
trn1    v3.4h, v3.4h, v3.4h

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

st1     {v0.8h-v3.8h}, [output], #64
ld4     {v0.h, v1.h, v2.h, v3.h}[2], [input], #8
ld4     {v0.h, v1.h, v2.h, v3.h}[0], [input]

sli     v0.2s, v0.2s, #16
sli     v1.2s, v1.2s, #16
sli     v2.2s, v2.2s, #16
sli     v3.2s, v3.2s, #16

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

st1     {v0.8h-v3.8h}, [output]

手写程序集:

aarch32

vld2.16     {d4, d6}, [input]!

vrev64.16   d4, d4
vrev64.16   d6, d6
vmov        d5, d4
vmov        d7, d6
vmov        q0, q2
vmov        q1, q3

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

vst1.16     {q0, q1}, [output]!
vst1.16     {q2, q3}, [output]!
vld4.16     {d2[2], d3[2], d4[2], d5[2]}, [input]!
vld4.16     {d2[0], d3[0], d4[0], d5[0]}, [input]

vsli.32     q1, q1, #16
vsli.32     q2, q2, #16

vmov        q0, q1
vmov        q3, q2

vswp        d1, d2
vswp        d6, d5

vst1.16     {q0, q1}, [output]!
vst1.16     {q2, q3}, [output]

aarch64

ld2     {v2.4h, v3.4h}, [input], #16

rev64   v2.4h, v2.4h
rev64   v3.4h, v3.4h
trn2    v0.4h, v2.4h, v2.4h
trn2    v1.4h, v3.4h, v3.4h
trn1    v2.4h, v2.4h, v2.4h
trn1    v3.4h, v3.4h, v3.4h

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

st1     {v0.8h-v3.8h}, [output], #64
ld4     {v0.h, v1.h, v2.h, v3.h}[2], [input], #8
ld4     {v0.h, v1.h, v2.h, v3.h}[0], [input]

sli     v0.2s, v0.2s, #16
sli     v1.2s, v1.2s, #16
sli     v2.2s, v2.2s, #16
sli     v3.2s, v3.2s, #16

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

st1     {v0.8h-v3.8h}, [output]

必须执行
vrev
,这会改变很多

下面是一个利用
vtbl
的“廉价”建议,如果要在循环中执行例程,这是有意义的

void foo(uint16_t *input, uint16_t *output)
{
    uint8_t *pSrc = (uint8_t *) input;
    uint8_t *pDst = (uint8_t *) output;

    uint8x8x2_t data;
    uint8x8_t d0, d1, d2, d3;

    const uint8x8_t mask0 = {8, 9, 8, 9, 0, 1, 0, 1};
    const uint8x8_t mask1 = {10, 11, 10, 11, 2, 3, 2, 3};
    const uint8x8_t mask2 = {12, 13, 12, 13, 4, 5, 4, 5};
    const uint8x8_t mask3 = {14, 15, 14, 15, 6, 7, 6, 7};

    data.val[0] = vld1_u8(pSrc++);
    data.val[1] = vld1_u8(pSrc++);

    d0 = vtbl2_u8(data, mask0);
    d1 = vtbl2_u8(data, mask1);
    d2 = vtbl2_u8(data, mask2);
    d3 = vtbl2_u8(data, mask3);

    vst1_u8(pDst++, d0);
    vst1_u8(pDst++, d0);
    vst1_u8(pDst++, d1);
    vst1_u8(pDst++, d1);
    vst1_u8(pDst++, d2);
    vst1_u8(pDst++, d2);
    vst1_u8(pDst++, d3);
    vst1_u8(pDst++, d3);
}

最后但并非最不重要的一点是,这可能是单个迭代中最快的例程,在汇编中编写时可以进一步提高

void foo(uint16_t *input, uint16_t *output)
{
    uint16x4x4_t data;

    data = vld4_lane_u16(input++, data, 2);
    data = vld4_lane_u16(input, data, 0);

    data.val[0] = vsli_n_u32(data.val[0], data.val[0], 16);
    data.val[1] = vsli_n_u32(data.val[1], data.val[1], 16);
    data.val[2] = vsli_n_u32(data.val[2], data.val[2], 16);
    data.val[3] = vsli_n_u32(data.val[3], data.val[3], 16);

    vst1_u16(output++, data.val[0]);
    vst1_u16(output++, data.val[0]);
    vst1_u16(output++, data.val[1]);
    vst1_u16(output++, data.val[1]);
    vst1_u16(output++, data.val[2]);
    vst1_u16(output++, data.val[2]);
    vst1_u16(output++, data.val[3]);
    vst1_u16(output, data.val[3]);
}

aarch32

vld2.16     {d4, d6}, [input]!

vrev64.16   d4, d4
vrev64.16   d6, d6
vmov        d5, d4
vmov        d7, d6
vmov        q0, q2
vmov        q1, q3

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

vst1.16     {q0, q1}, [output]!
vst1.16     {q2, q3}, [output]!
vld4.16     {d2[2], d3[2], d4[2], d5[2]}, [input]!
vld4.16     {d2[0], d3[0], d4[0], d5[0]}, [input]

vsli.32     q1, q1, #16
vsli.32     q2, q2, #16

vmov        q0, q1
vmov        q3, q2

vswp        d1, d2
vswp        d6, d5

vst1.16     {q0, q1}, [output]!
vst1.16     {q2, q3}, [output]

aarch64

ld2     {v2.4h, v3.4h}, [input], #16

rev64   v2.4h, v2.4h
rev64   v3.4h, v3.4h
trn2    v0.4h, v2.4h, v2.4h
trn2    v1.4h, v3.4h, v3.4h
trn1    v2.4h, v2.4h, v2.4h
trn1    v3.4h, v3.4h, v3.4h

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

st1     {v0.8h-v3.8h}, [output], #64
ld4     {v0.h, v1.h, v2.h, v3.h}[2], [input], #8
ld4     {v0.h, v1.h, v2.h, v3.h}[0], [input]

sli     v0.2s, v0.2s, #16
sli     v1.2s, v1.2s, #16
sli     v2.2s, v2.2s, #16
sli     v3.2s, v3.2s, #16

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

st1     {v0.8h-v3.8h}, [output]

vcombine实际上不会产生任何代码(大多数情况下),因为编译器非常聪明,能够以这种方式分配寄存器,所以vcombine的两个部分都已经在q寄存器的子部分中。哎呀,我忘记了
rev
output++只更新了一次,你需要移动4次!请注意,您使用
++
(用于输入和输出)的所有位置都不正确。在我的实际代码中,我需要得到4个包含这些通道的uint16x8\t寄存器,这是一次性的,因此,vtbl解决方案对我的情况不是最佳的。请根据您的第一个建议进行检查。尽管它使用vcombine和其他为arm64生成的asm是好的,但对armv7可能更好。它允许rev64和vtrn在一次操作中在q-REG上运行,而不是在d-HABLE上运行。注意,正如我在问题中所说的,我实际上需要得到q1、q2、q3等等,我将它们存储到输出中只是为了进行正确性检查,并确保编译器不会在godbolt上对它们进行优化。