有效计算arm neon

时间:2018-04-24 03:39:27

标签: c++ arm intrinsics neon

这是计算缓冲区中不同值数量的基本算法:

unsigned getCount(const uint8_t data[16])
{
    uint8_t pop[256] = { 0 };
    unsigned count = 0;
    for (int i = 0; i < 16; ++i)
    {
        uint8_t b = data[i];
        if (0 == pop[b])
            count++;
        pop[b]++;
    }
    return count;
}

这可以通过加载到q-reg并做一些魔术来有效地在霓虹灯中完成吗?或者,我是否可以有效地说data所有元素都相同,或者只包含两个不同的值或多于两个?

例如,使用vminv_u8vmaxv_u8我可以找到min和max元素,如果它们相等,我知道data具有相同的元素。如果没有,那么我可以vceq_u8使用最小值,vceq_u8使用最大值,然后vorr_u8这些结果,并比较我在结果中拥有所有1-s。基本上,在霓虹灯中,它可以通过这种方式完成。任何想法如何让它更好?

unsigned getCountNeon(const uint8_t data[16])
{
    uint8x16_t s = vld1q_u8(data);
    uint8x16_t smin = vdupq_n_u8(vminvq_u8(s));
    uint8x16_t smax = vdupq_n_u8(vmaxvq_u8(s));
    uint8x16_t res = vdupq_n_u8(1);
    uint8x16_t one = vdupq_n_u8(1);

    for (int i = 0; i < 14; ++i) // this obviously needs to be unrolled
    {
        s = vbslq_u8(vceqq_u8(s, smax), smin, s); // replace max with min
        uint8x16_t smax1 = vdupq_n_u8(vmaxvq_u8(s));
        res = vaddq_u8(res, vaddq_u8(vceqq_u8(smax1, smax), one));
        smax = smax1;
    }
    res = vaddq_u8(res, vaddq_u8(vceqq_u8(smax, smin), one));
    return vgetq_lane_u8(res, 0);
}

通过一些优化和改进,可以在32-48个氖指令中处理一个16字节的块。这可以做得更好吗?不大可能

我问这个问题的一些背景。当我正在研究算法时,我尝试了不同的处理数据的方法,而且我还不确定最终我会使用什么。可能有用的信息:

  • 每个16字节块的不同元素数
  • 每个16字节块重复大多数的值
  • 每块平均值
  • 每个区块的中位数
  • 光速?......这是一个笑话,它不能用16字节块的霓虹灯计算:)

所以,我尝试了一些东西,在使用任何方法之前,我想看看是否可以很好地优化这种方法。例如,每个块的平均值基本上是arm64上的memcpy速度。

1 个答案:

答案 0 :(得分:1)

如果您预计会有大量重复,并且可以 获得vminv_u8的水平分钟,这可能比标量更好。或者不是,也许NEON-&gt;循环条件的ARM停顿会使其失效。 &GT;。&LT;但是应该可以通过展开来缓解这种情况(并在寄存器中保存一些信息以确定你超出的程度)。

// pseudo-code because I'm too lazy to look up ARM SIMD intrinsics, edit welcome
// But I *think* ARM can do these things efficiently, 
// except perhaps the loop condition.  High latency could be ok, but stalling isn't

int count_dups(uint8x16_t v)
{
    int dups = (0xFF == vmax_u8(v));   // count=1 if any elements are 0xFF to start
    auto hmin = vmin_u8(v);

    while (hmin != 0xff) {
        auto min_bcast = vdup(hmin);  // broadcast the minimum
        auto matches = cmpeq(v, min_bcast);
        v |= matches;                 // min and its dups become 0xFF
        hmin = vmin_u8(v);
        dups++;
    }
    return dups;
}

这会将唯一值转换为0xFF,一次重复一组。

通过v / hmin的循环传输dep链停留在向量寄存器中;它只是需要NEON->整数的循环分支。

最小化/隐藏NEON->整数/ ARM惩罚

hmin上展开8而没有分支,结果为8个NEON寄存器。然后转移这8个值; back-to-back transfers of multiple NEON registers to ARM only incurs one total stall (无论Jake测试了什么,都是14个周期。)无序执行也可以隐藏这个失速的一些惩罚。然后使用完全展开的整数循环检查这8个整数寄存器。

将展开因子调整得足够大,以至于您通常不需要对大多数输入向量进行另一轮SIMD操作。如果几乎所有向量都有最多5个唯一值,则展开5而不是8。

不是将多个hmin结果转换为整数,而是将它们计入NEON 。如果你可以使用ARM32 NEON部分寄存器技巧将多个hmin值放在同一个向量中,那么将它们中的8个混合到一个向量中并进行比较是不相等的到0xFF。然后水平添加比较结果以获得-count

或者,如果您在单个矢量的不同元素中具有来自不同输入矢量的值,则可以使用垂直操作一次添加多个输入矢量的结果,而无需水平操作。

几乎可以肯定有这个优化空间,但我不太了解ARM,或者ARM性能细节。由于NEON->整数的性能损失很大,NEON很难用于任何条件,完全不同于x86。循环中带有NEON->整数的Glibc has a NEON memchr,但我不知道它是否使用它,或者它是否比标量更快。

加速对标量ARM版本的重复调用:

每次将256字节缓冲区归零会很昂贵,但我们并不需要这样做。 使用序列号以避免需要重置

  • 在每一组新元素之前:++seq;
  • 对于集合中的每个元素:

    sum += (histogram[i] == seq);
    histogram[i] = seq;     // no data dependency on the load result, unlike ++
    

您可以将直方图设为uint16_tuint32_t的数组,以避免在uint8_t seq换行时需要重新归零。但是它需要更多的缓存占用空间,因此可能只需重新调零每254个序列号最有意义。