ARM Neon:将非零字节的第n个位置存储在8字节矢量通道中

时间:2016-09-15 08:34:16

标签: assembly arm neon

我想转换一个霓虹灯64位矢量通道来获得非零(又名.0.0FF)8位值的第n个位置,然后填充其余的矢量零。以下是一些例子:

    hash_array.map{|k| {"array_value" => k['array_value'], 'inner_value' => k['inner_value'][0]} }

#=> [{"array_value"=>1, "inner_value"=>{"iwantthis"=>"forFirst"}}, {"array_value"=>2, "inner_value"=>{"iwantthis"=>"forSecond"}}]

我觉得它可能只有一两个移位的霓虹灯指令与另一个"好"向量。我怎么能这样做?

3 个答案:

答案 0 :(得分:1)

事实证明这并不简单。

天真有效的方法从简单地获取索引开始(只需使用位掩码加载0 1 2 3 4 5 6 7vand的静态向量)。但是,为了在输出向量的一端收集它们 - 在它们代表的输入通道的不同通道中 - 然后需要一些任意的置换操作。只有一条能够任意置换向量的指令vtbl(或vtbx,这基本上是相同的东西)。但是,vtbl采用目标顺序的源索引向量,结果与您尝试生成的完全相同。因此,为了产生最终结果,您需要使用最终结果,因此无法实现天真有效的解决方案; QED。

根本问题在于你正在做的是排序一个向量,这本身不是一个并行的SIMD操作。 NEON是一个专为媒体处理而设计的并行SIMD指令集,实际上并不适用于更常规矢量处理的任何数据相关/水平/散射 - 聚集操作。

为了证明这一点,我确实设法在纯NEON中执行此操作,根本没有任何标量代码,并且它是可怕的;我能想到的最好的“一位或两位移位NEON指令”是一些基于条件选择的旋转位掩码累积技巧。如果不清楚,我建议在调试器或模拟器中单步执行它的操作(example):

// d0 contains input vector
vmov.u8 d1, #0
vmov.u8 d2, #0
vmvn.u8 d3, #0
vdup.u8 d4, d0[0]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[1]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[2]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[3]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[4]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[5]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[6]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vsub.u8 d1, d1, d3
vdup.u8 d4, d0[7]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
vbic.u8 d1, d1, d3
// d1 contains output vector

作弊和使用循环(这需要在相反的方向上旋转d0,以便我们可以通过d0[0]访问每个原始通道)使其变小,但实际上并没有那么糟糕:

vmov.u8 d1, #0
vmov.u8 d2, #0
vmvn.u8 d3, #0
mov r0, #8
1:
vdup.u8 d4, d0[0]
vext.u8 d5, d2, d3, #7
vbit.u8 d3, d5, d4
subs r0, r0, #1
vext.u8 d0, d0, d0, #1
vsub.u8 d1, d1, d3
bne 1b
vbic.u8 d1, d1, d3

理想情况下,如果完全可以修改算法的其他部分以避免需要非常量的向量排列,那就改为做。

答案 1 :(得分:1)

您可以通过对矢量进行排序来完成此操作。这是一项比你预期的更复杂的操作,但是我还没有想出更好的东西。

根据00ff / d0个字节的列表以及0, 1, 2, ..., 7中的常量d1,您可以创建活动列的可排序列表使用vorn

vorn.u8 d0, d1, d0

现在d0的所有不需要的通道都已替换为0xff,其余的已被替换为其通道索引。从那里你可以对该列表进行排序,以便在最后聚集所有不需要的通道。

为此,您必须将列表扩展为16个字节:

vmov.u8 d1, #255

然后将它们分成奇数/偶数向量:

vuzp.u8 d0, d1

排序操作包含vmin / vmax在这些向量之间,然后是交错操作,然后是另一个vmin / vmax对,用于在不同<之间进行排序/ em>对,这样价值可以冒泡到他们适当的位置。像这样:

vmin.u8 d2, d0, d1
vmax.u8 d3, d0, d1
vsri.u64 d2, d2, #8   ; stagger the even lanes (discards d0[0])
vmax.u8 d4, d2, d3    ; dst reg would be d0, but we want d0[0] back...
vmin.u8 d1, d2, d3
vsli.u64 d0, d4, #8   ; revert the stagger, and restore d0[0]

这实现了整个网络的两个阶段,整个块必须重复四次(八个阶段),以使d0[7]中的某些内容可以一直向下冒充d0[0]在极端情况下,最后一个字节是唯一的非零输入,或者d0[0]如果第一个字节是唯一的零输入则到d0[7]

完成排序后,将结果重新拼接在一起:

vzip.u8 d0, d1

因为你想在剩下的小巷中使用零:

vmov.u8 d1, #0
vmax.s8 d0, d1

现在d0应包含结果。

如果您查看维基百科的sorting network页面,您会发现八个车道的理论最小深度仅为六个阶段(六对vmin / {{1} }),因此有可能找到一组排列(替换我的vmaxvsli操作),这些排列实现了六阶段排序网络,而不是八阶段插入/选择/ vbubble排序我已经实施了。如果确实存在,并且与NEON的置换操作兼容,那么它肯定值得一试,但我没有时间去看。

另请注意,排序总共需要16个字节,这比您需要的多,如果使用q个寄存器,则可以使用32个字节......所以这距离最大吞吐量还有很长的路要走

哦,即使在这种配置中,我认为你不需要排序的最后阶段。为读者留下的练习。

答案 2 :(得分:1)

我计划使用可变移位的分而治之技术来实现这一目标。 在每个步骤中,输入都被视为“高”和“低”部分,其中“高”部分需要先右移(向最低有效位),再移0或1个字节,然后移0-2个字节,然后0-4字节。

该解决方案允许在所有指令中使用'q'变体,从而允许并行进行两次独立压缩。

///  F F 0 F F 0 0 F   mask
///  7 6 - 4 3 - - 0   mask & idx
///  7 6 - 4 - 3 - 0   pairwise shift.16 right by 1-count_left bytes
///    2   1   1   1   count_left + count_right = vpaddl[q]_s8 -> s16
///  - 7 6 4 - - 3 0   pairwise shift.32 right by 2-count_left bytes
///        3       2   count_left + count_right = vpaddl[q]_s16 -> s32
///  - - - 7 6 4 3 0   shift.64 right by 4 - count_left bytes
///                5   number of elements to write = vpaddl[q]_s32 -> s64

第一步无需实际移位即可

int8x8_t step1(int8x8_t mask) {
    auto data = mask & vcreate_u8(0x0706050403020100ull);
    auto shifted = vrev16_u8(data);
    return vbsl_u8(vtst_s16(mask, vdup_n_s16(1)), data, shifted);
}

下一步需要隔离每个uint32_t通道的高16位和低16位,将高16位,-8位或0位移位,然后与隔离的低位组合。

int8x8_t step2(int8x8_t mask, int16x4_t counts) {
    auto top = vshr_n_u32(top, 16);
    auto cnt = vbic_s32(counts, vcreate_u32(0xffff0000ffff0000ull));
    auto bot = vbic_u32(mask, vcreate_u32(0xffff0000ffff0000ull));
    top = vshl_u32(top, cnt);
    return vorr_u8(top, bot); 
}

第三步需要转移64位元素。

int8x8_t step3(int8x8_t mask, int32x4_t counts) {
    auto top = vshr_n_u64(top, 32);
    auto cnt = vbic_s64(counts, vcreate_s32(0xffffffff00000000ull));
    auto bot = vbic_u64(mask, vcreate_u32(0xffffffff00000000ull));
    top = vshl_u64(top, cnt);
    return vorr_u8(top, bot); 
}

完整解决方案:

auto cnt8 = vcnt_s8(mask);
mask = step1(mask);
auto counts16 = vpaddl_s8(cnt8, cnt8);
mask = step2(mask, counts16);
auto counts32 = vpaddl_s16(counts16, counts16);
mask = step3(mask, counts32);
auto counts64 = vpaddl_s32(counts32, counts32);

最终的“ counts64”应实际预先计算,因为需要将计数转移到通用寄存器中,因为该寄存器用于在 bytes 中增加流写入指针:

vst1_u8(ptr, mask); ptr += count64 >> 3;

一个渐近更好的版本实际上会尝试获取64(+ 64 =字节的掩码值,将这些字节压缩为位模式(如Intel的movmaskb中一样),然后对find leading zeros + clear leading one或{{1}使用8次迭代}。

这可以通过每次迭代5条指令来实现:

find + toggle least significant set bit

这8个索引[0..7]需要转置为8x8矩阵,然后顺序写入;每64 + 64字节的指令总数将接近64条指令,或每输出字节0.5条指令。