如何清除霓虹灯中除第一个非零车道之外的所有车道?
Posted
技术标签:
【中文标题】如何清除霓虹灯中除第一个非零车道之外的所有车道?【英文标题】:How to clear all but the first non-zero lane in neon? 【发布时间】:2018-07-08 06:36:07 【问题描述】:我在 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 中更有效地做到这一点?
【问题讨论】:
所以您想将除第一个非零车道之外的所有车道都归零?它们总是 0 / -1(您可以直接用作 AND 或 AND-NOT 掩码),还是第一个或后面的元素可能有其他非零值? 请注意,您可以简化示例实现,以省略已使用if()
检查的归零元素。
是的,值只有 0
或 -1
,我想最终得到一个只有一个设置为 -1 的霓虹灯寄存器(不一定必须是第一个一)。
@PeterCordes clearmask 简化,并添加了匹配的 neon impl
【参考方案1】:
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
从两个向量的串联中获取一个窗口,所以我们正在创建掩码
v0 = [ d c b a ]
v2 = [ !c !b !a -1 ]
v3 = [ !b !a -1 -1 ]
v4 = [ !a -1 -1 -1 ]
如果前面的任何元素非零,则最高元素 (d
) 将归零。
如果其前面的任何元素(a
或 b
)非零,则将第二高元素(c
)归零。以此类推。
如果元素保证为 0 或 -1,mvn
也可以代替与零进行比较。
【讨论】:
你能解释一下那里发生了什么吗?另外,输出是什么?我认为它必须是q0
,但最后两个vand
指令似乎毫无意义,因为它们没有触及q0
。
@Pavel d1 是 q0 的上半部分。是的,q0 既是输入又是输出。
我将添加aarch64
版本。这应该更容易理解,尽管时间更长且效率更低。
这非常有效。我需要在纸上画画才能看到发生了什么,但它确实有效:) 顺便说一下,对于 arm64,neon intrisic 版本似乎是produce better results:产生相同数量的指令,但它使用的寄存器更少(不触摸v4
和v31
)
不错的算法,我知道我的答案看起来很笨拙,但我没有立即看到这个逻辑。添加了对逻辑的解释。 (我不得不查找一些 ARM 指令以了解他们做了什么来查看您正在创建的掩码。)顺便说一句,x86 版本可以使用 palignr
做同样的事情,这与 vext.8
完全相同。
【参考方案2】:
我的想法与您未注释的代码几乎相同:将反转的元素广播为 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
与其他元素中的全1 替换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
【讨论】:
这似乎不会产生比我发布的幼稚 impl 更好的代码。查看 Jake 的解决方案,不确定它是如何工作的,但它产生的操作码减少了两倍。 @Pavel:您是否正在查看包含加载常量的独立版本?我的回答解释说,只有当它可以内联到循环中并将常量设置提升到循环之外时才好;从 asm 应该很明显。不过,Jake 的实现更好。 是的,这是我在函数结束时需要做的一次性事情。我可能会在手臂上做这部分,但我需要移动到手臂,然后回到霓虹灯,我试图通过清除面具使其在霓虹灯中更好。以上是关于如何清除霓虹灯中除第一个非零车道之外的所有车道?的主要内容,如果未能解决你的问题,请参考以下文章