有效计算 arm neon 中 16 字节缓冲区中不同值的数量

Posted

技术标签:

【中文标题】有效计算 arm neon 中 16 字节缓冲区中不同值的数量【英文标题】:Efficiently count number of distinct values in 16-byte buffer in arm neon 【发布时间】:2018-04-24 03:39:27 【问题描述】:

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

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 并做一些魔术来有效地在 neon 中完成吗?或者,我可以有效地说data 的所有元素都相同,还是只包含两个不同的值或两个以上?

例如,使用vminv_u8vmaxv_u8 我可以找到最小和最大元素,如果它们相等,我知道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);

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

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

每 16 字节块的不同元素计数 每 16 字节块重复次数最多的值 平均每块 每块的中位数 光速?.. 开个玩笑,它不能用 16 字节块在霓虹灯中计算:)

所以,我正在尝试一些东西,在我使用任何方法之前,我想看看该方法是否可以得到很好的优化。例如,arm64 上的平均每块速度基本上就是 memcpy 速度。

【问题讨论】:

无需在pop[b]++ 中进行post incr,只需这样做pop[b]=1,因为它们不需要确定每个字节的重复数。这可以通过避免读取改变写入来帮助缓存。 就这个问题而言,它并没有太大的相关性,在我的实际代码中我确实使用它,所以它在这里结束了。 uint8_t data[16] 是如何16字节缓冲区的 半相关:Fallback implementation for conflict detection in AVX2 是关于找出是否有任何重复的(32 位)元素,而不是找出它们在哪里或它们是什么。 (当常见的情况是没有重复时,可用作对分散/聚集冲突的乐观快速路径检查)。我只是在结果上使用 3 次随机播放、4 次比较和 3 次 OR 将每个元素与其他元素进行了比较。 @Pavel - 你的“小范围”问题会得到误导性的答案。针对较大数据集的优化解决方案将 8/16 问题作为最后一步。如果处理得当,(通常效率低下的)最终输出步骤将被大多数数据的有效解决方案所掩盖。以你的方式提问,你是在浪费专家的时间/经验来解决问题的错误结果。 【参考方案1】:

如果您预计会有很多重复项,并且可以有效地使用vminv_u8 获得水平最小值,那么这可能比标量更好。或者不是,也许 NEON->ARM 会因为循环条件而停止工作。 >// 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->integer。


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

展开 8,hmin 上没有分支,将结果留在 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->integer 的性能损失很大,这与 x86 完全不同。 Glibc has a NEON memchr 循环中带有 NEON->integer,但我不知道它是否使用它或者它是否比标量更快。


加快对标量 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 个序列号重新归零最有意义。

【讨论】:

顺便说一句,this answer 是让我想到像这样奇怪的方式使用vmin 的灵感的一部分。相关:x86 有phminposuw,所以它只能对 16 位元素执行此操作。 x86 很少有高效的横向指令,其他包括psadbwpmaddubsw. phaddd` 存在但效率不高。 我目前正在重新考虑采用我正在开发的算法的方法(这个和其他手臂霓虹灯问题都与那个有关)。经过一些草率的优化(没有太努力)后,我得到的结果不是我所希望的,我需要它快 100-1000 ;)基本上,一些最常见/预期输入的启发式和快捷方式可以跳过它即使结果不是最好的。 我认为你对 SIMD 的想象是错误的。 Neon 基本上是一个非常强大的 DSP,你的问题不是信号类的 @Pavel:对于重复使用,您可以将设置为 1 的 16 个条目归零,使用 ARM 指令再次循环源向量。或者使用一个序列号来避免需要重置:seq++在每组16个字节之后,然后sum += (histogram[i] == seq); histogram[i] = seq;

以上是关于有效计算 arm neon 中 16 字节缓冲区中不同值的数量的主要内容,如果未能解决你的问题,请参考以下文章

在 arm neon 中有效地重新洗牌和组合 16 个 3 位数字

与 ARM Neon vtbx 的字节顺序混淆

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

如何使用 Neon Extension 有效地反转汇编语言 ARM 中的数组?

ARM NEON 到 aarch64

Neon 在 Intrinsics 中的校验和代码实现