如何清除霓虹灯中除第一个非零车道外的所有车道?

How to clear all but the first non-zero lane in neon?

我在 uint32x4_t 霓虹灯寄存器中有一个面具。在此掩码中,至少设置了 4 个整数中的 1 个(例如 0xffffffff),但是,我可能会遇到寄存器中设置了多个项目的情况。如何确保只设置一个?

C伪代码:

uint32x4_t clearmask(uint32x4_t m)
{
         if (m[0]) { m[1] = m[2] = m[3] = 0; }
    else if (m[1]) { m[2] = m[3] = 0; }
    else if (m[2]) { m[3] = 0; }
    return m;
}

基本上我想清除除了一条设定车道之外的所有车道。明显的 straightforward implementation in neon 可能是:

uint32x4_t cleanmask(uint32x4_t m)
{
    uint32x4_t mx;
    mx = vdupq_lane_u32(vget_low_u32(vmvnq_u32(m)), 0);
    mx = vsetq_lane_u32(0xffffffff, mx, 0);
    m = vandq_u32(m, mx);

    mx = vdupq_lane_u32(vget_low_u32(vmvnq_u32(m)), 1);
    mx = vsetq_lane_u32(0xffffffff, mx, 1);
    m = vandq_u32(m, mx);

    mx = vdupq_lane_u32(vget_high_u32(vmvnq_u32(m)), 0);
    mx = vsetq_lane_u32(0xffffffff, mx, 2);
    m = vandq_u32(m, mx);

    return m;
}

如何在 arm neon 中更有效地完成这项工作?

我的想法与您未注释的代码几乎相同:如果设置了反向元素,则将反向元素作为 AND 掩码广播到零后面的元素,否则保持向量不变。

但是如果你在一个循环中使用它并且有 3 个备用向量寄存器,你不能只用一个元素与 XOR,而不是 MVN + 设置一个元素。

vdupq_lane_u32(vget_low_u32(m), 1); 似乎可以像 vdup.32 q9, d16[1] 一样有效地编译,并且我的那部分代码与您的代码相同(但没有 mvn)。

不幸的是,这是一个很长的串行依赖链;我们正在根据 AND 结果创建下一个掩码,因此没有 ILP。我看不出有什么好方法可以降低延迟,同时仍能获得所需的结果。

uint32x4_t cleanmask_xor(uint32x4_t m)
{
    //                 {  a    b    c   d }
    uint32x4_t maska = {  0, ~0U, ~0U, ~0U};
    uint32x4_t maskb = {~0U,   0, ~0U, ~0U};
    uint32x4_t maskc = {~0U, ~0U,   0, ~0U};

    uint32x4_t tmp = vdupq_lane_u32(vget_low_u32(m), 0);
    uint32x4_t aflip = tmp ^ maska;
    m &= aflip;  // if a was non-zero, the rest are zero

    tmp = vdupq_lane_u32(vget_low_u32(m), 1);
    uint32x4_t bflip = tmp ^ maskb;
    m &= bflip;  // if b was non-zero, the rest are zero

    tmp = vdupq_lane_u32(vget_high_u32(m), 0);
    uint32x4_t cflip = tmp ^ maskc;
    m &= cflip;  // if b was non-zero, the rest are zero

    return m;
}

(Godbolt)

/* design notes
  [ a   b   c   d ]
  [ a  ~a  ~a  ~a ] 

&:[ a   0   0   0 ]
or[ 0   b   c   d ]

= [ e   f   g   h  ]
  [ ~f  f   ~f  ~f ]  // not b, because f can be zero when b isn't

= [ i   j   k   l ]
  ...
*/

随着负载从循环中提升,这只有 9 条指令而不是 12 条指令,因为我们跳过了 vmov.32 d1[0], r3 或任何在每个掩码中插入 -1 的指令。 (一个元素与自身的与运算等同于与 -1U 的与运算。)veor 与其他元素中的全一替换 vmvn.

clang 在加载多个向量常量方面似乎效率低下:它分别设置每个地址,而不是仅仅将它们彼此靠近地存储在它可以从一个基指针到达的地方。因此,您可能需要考虑创建 3 个常量的替代策略。

#if 1
    // clang sets up the address of each constant separately
    //                 {  a    b    c   d }
    uint32x4_t maska = {  0, ~0U, ~0U, ~0U};
    uint32x4_t maskb = {~0U,   0, ~0U, ~0U};
    uint32x4_t maskc = {~0U, ~0U,   0, ~0U};
#else
    static const uint32_t maskbuf[] = 
      { -1U, -1U, 0, -1U, -1U, -1U};
    // unaligned loads.
    // or load one + shuffle?
#endif

Very simple:

vceq.u32    q1, q0, #0
vmov.i8     d7, #0xff
vext.8      q2, q3, q1, #12

vand        q0, q0, q2
vand        d1, d1, d2
vand        d1, d1, d4

总共 6 条指令,如果您可以将 q3 保持为常数,则为 5 条。

下面的aarch64版本一定更容易理解:

cmeq    v1.4s, v0.4s, #0
movi    v31.16b, #0xff

ext     v2.16b, v31.16b, v1.16b, #12
ext     v3.16b, v31.16b, v1.16b, #8
ext     v4.16b, v31.16b, v1.16b, #4

and     v0.16b, v0.16b, v2.16b
and     v0.16b, v0.16b, v3.16b
and     v0.16b, v0.16b, v4.16b

这是如何运作的

ext/vext 从两个向量的串联中获取 window,因此我们正在创建掩码

v0 = [  d   c   b   a ]

v2 = [ !c  !b  !a  -1 ]
v3 = [ !b  !a  -1  -1 ]
v4 = [ !a  -1  -1  -1 ]

如果前面的任何元素不为零,则最高元素 (d) 归零。

如果其前面的任何元素(ab)非零,则第二高的元素 (c) 将被置零。等等。


对于保证为 0 或 -1 的元素,mvn 也可以代替与零进行比较。