使用 SIMD 找出两个元素的最大差异

Posted

技术标签:

【中文标题】使用 SIMD 找出两个元素的最大差异【英文标题】:Using SIMD to find the biggest difference of two elements 【发布时间】:2018-09-24 19:32:35 【问题描述】:

我编写了一个算法来获取 std::vector 中两个元素之间的最大差异,其中两个值中的较大者必须位于比较低值更高的索引处。

    unsigned short int min = input.front();
    unsigned short res = 0;

    for (size_t i = 1; i < input.size(); ++i)
    
        if (input[i] <= min)
        
            min = input[i];
            continue;
        

        int dif = input[i] - min;
        res = dif > res ? dif : res;
    

    return res != 0 ? res : -1;

是否可以使用 SIMD 优化此算法?我是 SIMD 的新手,到目前为止我还没有成功

【问题讨论】:

【参考方案1】:

您没有指定任何特定的架构,因此我将使用英文描述的算法来保持这个大部分架构中立。但它需要一个 SIMD ISA,它可以有效地根据 SIMD 比较结果进行分支,以检查通常为真的条件,例如 x86,但不是真正的 ARM NEON。

这不适用于 NEON,因为它没有等效的移动掩码,而且 SIMD -> 整数会导致许多 ARM 微架构停止。


循环遍历数组时的正常情况是一个元素或整个 SIMD 元素向量,不是新的min,而不是diff 候选强>。我们可以快速浏览这些元素,只有在有新的min 时才会放慢速度以获取详细信息。这就像 SIMD strlen 或 SIMD memcmp,除了不是在第一次搜索命中时停止,我们只是对一个块进行标量然后继续。


对于输入数组的每个向量 v[0..7](假设每个向量有 8 个 int16_t 元素(16 字节),但这是任意的):

SIMD 比较 vmin &gt; v[0..7],并检查所有元素是否为真。 (例如 x86 _mm_cmpgt_epi16 / if(_mm_movemask_epi8(cmp) != 0)如果某处有新的min,我们有一个特殊情况:旧的最小值适用于某些元素,但新的最小值适用于其他元素。并且向量中可能有多个新的最小更新,并且在这些点中的任何一个点都有新的差异候选。

所以用标量代码处理这个向量(更新一个标量 diff,它不需要与向量 diffmax 同步,因为我们不需要位置)。

完成后将最终的min 广播到vmin。或者做一个 SIMD 水平 min,这样以后的 SIMD 迭代的乱序执行就可以开始,而无需等待来自标量的 vmin。如果标量代码是无分支的,则应该可以正常工作,因此标量代码中不会出现导致后续向量工作被抛出的错误预测。

作为替代方案,SIMD 前缀和类型的事物(实际上是前缀最小值)可以生成 vmin,其中每个元素都是该点之前的最小值。 (parallel prefix (cumulative) sum with SSE) .您可以总是这样做以避免任何分支,但如果新的最小候选人很少,那么它很昂贵。不过,它在难以分支的 ARM NEON 上可能是可行的。

如果没有新的最小值,SIMD 压缩最大值 diffmax[0..7] = max(diffmax[0..7], v[0..7]-vmin)。 (如果您使用无符号最大值来处理整个范围,请使用饱和减法,这样您就不会得到较大的无符号差异。)

在循环结束时,对diffmax 向量执行 SIMD 水平最大值。请注意,由于我们不需要 需要最大差异的位置,因此我们不需要在找到新的候选者时更新循环内的所有元素。我们甚至不需要保持标量特例 diffmax 和 SIMD vdiffmax 彼此同步,只需在最后检查以获取标量和 SIMD 最大差异的最大值。


SIMD min/max 与水平求和基本相同,只是您使用 packed-max 而不是 packed-add。对于 x86,请参阅Fastest way to do horizontal float vector sum on x86。

或者在带有 SSE4.1 的 x86 上用于 16 位整数元素,phminposuw / _mm_minpos_epu16 可用于最小值或最大值、有符号或无符号,并对输入进行适当调整。 max = -min(-diffmax)。您可以将 diffmax 视为无符号,因为它已知为非负数,但 Horizontal minimum and maximum using SSE 显示了如何翻转符号位以将有符号范围移位为无符号并返回。


每次我们找到一个新的min 候选者时,我们可能会得到一个分支错误预测,或者我们发现新的min 候选者过于频繁,以至于效率不高。

如果经常期望新的min 候选者,使用较短的向量可能会很好。或者在发现当前向量中有一个新的-min,然后使用更窄的向量仅在更少的元素上进行标量。在 x86 上,您可以使用 bsf(向前位扫描)来查找哪个元素具有第一个 new-min。这使您的标量代码对向量比较掩码具有数据依赖性,但如果错误预测到它的分支,则比较掩码将准备就绪。否则,如果分支预测能够以某种方式找到向量需要标量回退的模式,则预测+推测执行将破坏该数据依赖性。


未完成/损坏(由我)示例改编自 @harold 已删除的完全无分支版本的答案,该版本为 x86 SSE2 动态构建了一个最小到该元素的向量。

(@harold 用 suffix-max 而不是 min 写的,我想这就是他删除它的原因。我将它从 max 部分转换为 min。)

x86 的无分支内在函数版本可能看起来像这样某种东西。但是,除非您预期某种斜率或趋势会使新的 min 值频繁出现,否则分支可能会更好。

// BROKEN, see FIXME comments.
// converted from @harold's suffix-max version

int broken_unfinished_maxDiffSSE(const std::vector<uint16_t> &input) 
    const uint16_t *ptr = input.data();

    // construct suffix-min
    // find max-diff at the same time
    __m128i min = _mm_set_epi32(-1);
    __m128i maxdiff = _mm_setzero_si128();

    size_t i = input.size();
    for (; i >= 8; i -= 8) 
        __m128i data = _mm_loadu_si128((const __m128i*)(ptr + i - 8));

   // FIXME: need to shift in 0xFFFF, not 0, for min.
   // or keep the old data, maybe with _mm_alignr_epi8
        __m128i d = data;
        // link with suffix
        d = _mm_min_epu16(d, _mm_slli_si128(max, 14));
        // do suffix-min within block.
        d = _mm_min_epu16(d, _mm_srli_si128(d, 2));
        d = _mm_min_epu16(d, _mm_shuffle_epi32(d, 0xFA));
        d = _mm_min_epu16(d, _mm_shuffle_epi32(d, 0xEE));
        max = d;

        // update max-diff
        __m128i diff = _mm_subs_epu16(data, min);  // with saturation to 0
        maxdiff = _mm_max_epu16(maxdiff, diff);
    

    // horizontal max
    maxdiff = _mm_max_epu16(maxdiff, _mm_srli_si128(maxdiff, 2));
    maxdiff = _mm_max_epu16(maxdiff, _mm_shuffle_epi32(maxdiff, 0xFA));
    maxdiff = _mm_max_epu16(maxdiff, _mm_shuffle_epi32(maxdiff, 0xEE));
    int res = _mm_cvtsi128_si32(maxdiff) & 0xFFFF;

    unsigned scalarmin = _mm_extract_epi16(min, 7);  // last element of last vector
    for (; i != 0; i--) 
        scalarmin = std::min(scalarmin, ptr[i - 1]);
        res = std::max(res, ptr[i - 1] - scalarmin);
    

    return res != 0 ? res : -1;

如果我们处理最后一个完整向量 min 之间的重叠,我们可以用最终未对齐向量替换标量清理。

【讨论】:

那样我做了那个“带后缀的链接”步骤,通过切换到min 被破坏无论如何都是一个不好的方法,真的应该广播然后把它放在 after 块内的东西,使循环携带依赖更快 @harold:哦,好点!这甚至不是 shuffle 的瓶颈,而是 shuffle->max->... dep 链上的瓶颈。我想包含某种代码作为内在函数外观的示例,以便 OP 谷歌更多。因此,我将把那个不太好的代码块留在那里,并带有警告 cmets。如果您有时间改进/修复它,请随时编辑我的答案,或者最好取消删除您自己的答案。

以上是关于使用 SIMD 找出两个元素的最大差异的主要内容,如果未能解决你的问题,请参考以下文章

分而治之算法找到两个有序元素之间的最大差异

ArrayIndexOutOfBoundsException,同时找到数组中两个连续元素之间的最大差异

分而治之以找到二维数组中两个有序元素之间的最大差异

找出两个整型数组中的公共元素的最大值

SIMD/SSE:短点积和短最大值

找出两个字符串中最大的相同子字符串