【问题标题】:Optimal SSE unsigned 8 bit compare最佳 SSE 无符号 8 位比较
【发布时间】:2016-02-22 19:12:18
【问题描述】:

我正在尝试找到使用 SSE(最高 SSE 4.2)执行 8 位无符号比较的最佳方式。

我正在处理的最常见情况是比较 > 0U,例如

_mm_cmpgt_epu8(v, _mm_setzero_si128())                // #1

(当然也可以认为是对非零的简单检验。)

但我也对更一般的情况有些感兴趣,例如

_mm_cmpgt_epu8(v1, v2)                                // #2

第一种情况可以用 2 条指令实现,使用各种不同的方法,例如与 0 比较,然后反转结果。第二种情况通常需要 3 条指令,例如从两个操作数中减去 128 并执行有符号比较。 (各种3指令解决方案见this question。)

理想情况下,我正在寻找#1 的单指令解决方案和#2 的双指令解决方案。如果这些都不可能,那么我也有兴趣思考各种可能的 2 或 3 指令实现中哪一个在现代 Intel CPU(Sandy Bridge、Ivy Bridge、Haswell)上最有效。


迄今为止案例 #2 的最佳实现:

    1. 与无符号最大值比较等于并取反:

#define _mm_cmpgt_epu8(v0, v1) \ _mm_andnot_si128(_mm_cmpeq_epi8(_mm_max_epu8(v0, v1), v1), \ _mm_set1_epi8(-1))

两条算术指令 + 一条按位 = 1.33 吞吐量。

    1. 反转两个参数的符号位(== 减 128)并使用有符号比较:

#define _mm_cmpgt_epu8(v0, v1) \ _mm_cmpgt_epi8(_mm_xor_si128(v0, _mm_set1_epi8(-128)), \ _mm_xor_si128(v1, _mm_set1_epi8(-128)))

一条算术指令 + 两条按位 = 1.16 吞吐量。


案例 #1 的最佳实现,源自上述案例 #2 的实现:

  • 1.

#define _mm_cmpgtz_epu8(v0) \ _mm_andnot_si128(_mm_cmpeq_epi8(v0, _mm_set1_epi8(0)), \ _mm_set1_epi8(-1))

一条算术指令 + 一条按位 = 0.83 吞吐量。

  • 2.

#define _mm_cmpgtz_epu8(v0) \ _mm_cmpgt_epi8(_mm_xor_si128(v0, _mm_set1_epi8(-128)), \ _mm_set1_epi8(-128)))

一条算术指令 + 一条按位 = 0.83 吞吐量。

【问题讨论】:

  • 显然有“异或而不是减法”,吞吐量稍好..除此之外,我什么也没得到,#1 的一条指令对我来说似乎非常乐观
  • A) #2 的非严格版本(又名 ge)可以用 your own idea 在 2 条指令中完成。 B) 一条指令_mm_subs_epu8 足以将问题#2 简化为问题#1。 C) 使用superoptimization 可以一劳永逸地解决这个问题。我认为可以对 2 个 SSE 指令的所有序列执行暴力搜索,只需 2 个寄存器和一组有限的常量(例如由_mm_set1_epi8 生成的那些)。
  • 英特尔需要停止将 AVX512 悬挂在我们面前,并像猫面前的玩具一样将其拉开。
  • AMD 在很久以前就提供了 XOP。
  • @PeterCordes:是的,这通常是 8 或 16 次循环迭代的序列,因此可以假定任何掩码或常量是“免费的”。 CPU 仅限 Intel,Sandy Bridge 及以上版本。

标签: c x86 sse simd sse4


【解决方案1】:

我有一个想法在两个指令中做>=:

  • 减去无符号饱和度
  • 与零比较

不过,这对 > 没有帮助。

此外,它几乎等同于 ErmIg 的 SIMD Library 答案 (max_epu8(a,b) -> cmpeq with a),但更糟糕的是,它需要一个归零的寄存器。不过,这适用于 SSE2,而不是 SSE4.1。 psubusb 与 pminusb 在相同的端口上运行。


此答案的先前版本有一个错误的想法,即 b-a 的符号位设置为 iff a>b。但它实际上是需要测试的左侧一位:进位标志/位(对于压缩整数 SIMD 不存在)。

请参阅编辑历史记录,了解有关使用 pshufb(否定结果)或 pblendvb(对于非 VEX 版本可能是 Skylake 上的单微指令)将符号位广播到元素其余部分的一些想法.

【讨论】:

  • 谢谢 Peter - 我将把各种不同的想法插入到 SB/IB/Haswell 上的测试工具和基准测试中,看看是否有明显的赢家。我在相当关键的代码块中使用了这些东西,所以即使我们可以节省一个周期的一小部分,它也将是一场胜利。
  • @PaulR:这可能是最好的选择,对整个循环进行基准测试。即使使用 uop 循环缓冲区,代码对齐有时仍然很重要,并且执行端口压力将取决于上下文。
  • 确实是的,并且围绕这些比较还有其他代码我没有包括在内,因此最终的调度等可能会与实践中的预期不同。如果基准数据看起来很有趣,我会将其总结为答案。
  • @PaulR:除非您的其余代码无法保持 port0 饱和,否则我预测两个版本的性能基本相同。只使用set1(-1) 的那个将有更少的开销来设置它的常量(因为它是一个 ALU 操作来生成一个所有的向量,而不是一个负载。)
  • 加一个用于思考问题而不仅仅是发布代码。
【解决方案2】:

本着从 SIMD 库复制代码的精神,Agner Fog 的 Vector Class Library (C++) 就是这样做的:

// vector operator >= : returns true for elements for which a >= b (unsigned)
static inline Vec16cb operator >= (Vec16uc const & a, Vec16uc const & b) {
#ifdef __XOP__  // AMD XOP instruction set
    return _mm_comge_epu8(a,b);
#else  // SSE2 instruction set
    return _mm_cmpeq_epi8(_mm_max_epu8(a,b),a); // a == max(a,b)
#endif
}

// vector operator <= : returns true for elements for which a <= b (unsigned)
static inline Vec16cb operator <= (Vec16uc const & a, Vec16uc const & b) {
    return b >= a;
}

// vector operator > : returns true for elements for which a > b (unsigned)
static inline Vec16cb operator > (Vec16uc const & a, Vec16uc const & b) {
#ifdef __XOP__  // AMD XOP instruction set
    return _mm_comgt_epu8(a,b);
#else  // SSE2 instruction set
    return Vec16cb(Vec16c(~(b >= a)));
#endif
}

// vector operator < : returns true for elements for which a < b (unsigned)
static inline Vec16cb operator < (Vec16uc const & a, Vec16uc const & b) {
    return b > a;
}

// vector operator ~ : bitwise not
static inline Vec16uc operator ~ (Vec16uc const & a) {
    return Vec16uc( ~ Vec128b(a));
}

按位不定义为

// vector operator ~ : bitwise not
static inline Vec128b operator ~ (Vec128b const & a) {
    return _mm_xor_si128(a, _mm_set1_epi32(-1));
}

【讨论】:

  • 谢谢 - 我不知道 AMD 在 XOP 中有 8 位无符号比较。我目前只针对英特尔,所以不幸的是它在这种情况下没有帮助,但了解它以备将来参考很有用。
  • @PaulR,是的,XOP 填补了标量指令具有的许多(但不是全部)缺失的基本整数 SIMD 操作。英特尔还没有实现这些真是太遗憾了(我的 AVX512 在哪里!!!???)。几乎就像他们这样做是出于恶意,没有给予 AMD 信用并惩罚消费者。对我来说,他们已经摆脱了this 这么久,这对我来说毫无意义。具有讽刺意味的是,也许我们现在没有 AVX512 的原因可能是他们想让 AMD 迎头赶上。但是如果 Xen 删除了 XOP 但注释不给 AVX512 那将是一个退步。
  • 是的,我仍然怀念 PowerPC 和 AltiVec 的美好时光 - 更加正交的 SIMD,没有所有的差距。
  • @PaulR,恭喜您获得了 SSE 金牌(这也是第一个也是唯一一个获得银牌的)!
  • 谢谢 - 我今天感觉有点独特和特别(这对于星期一的早晨来说是不寻常的)! ;-)
【解决方案3】:

有一个来自Simd Library的例子:

    const __m128i K_INV_ZERO = SIMD_MM_SET1_EPI8(0xFF);//_mm_set1_epi8(-1);

    SIMD_INLINE __m128i NotEqual8u(__m128i a, __m128i b)
    {
        return _mm_andnot_si128(_mm_cmpeq_epi8(a, b), K_INV_ZERO);
    }

    SIMD_INLINE __m128i Greater8u(__m128i a, __m128i b)
    {
        return _mm_andnot_si128(_mm_cmpeq_epi8(_mm_min_epu8(a, b), a), K_INV_ZERO);
    }

    SIMD_INLINE __m128i GreaterOrEqual8u(__m128i a, __m128i b)
    {
        return _mm_cmpeq_epi8(_mm_max_epu8(a, b), a);
    }

    SIMD_INLINE __m128i Lesser8u(__m128i a, __m128i b)
    {
        return _mm_andnot_si128(_mm_cmpeq_epi8(_mm_max_epu8(a, b), a), K_INV_ZERO);
    }

    SIMD_INLINE __m128i LesserOrEqual8u(__m128i a, __m128i b)
    {
        return _mm_cmpeq_epi8(_mm_min_epu8(a, b), a);
    }

【讨论】:

  • 谢谢,是的,我已经有一个类似的实现,其中包含三个针对_mm_cmpgt_epu8 的指令(上面示例中的Greater8u)。看起来到目前为止我能做的最好的事情是两条算术指令,吞吐量为 0.5 + 一条按位计算,吞吐量为 0.33,总共为 1.33。我希望有一个更好的解决方案,即使它仍然是三个指令(例如一个算术和两个按位)。
猜你喜欢
  • 2012-01-16
  • 1970-01-01
  • 2016-10-16
  • 1970-01-01
  • 2017-10-28
  • 2019-07-11
  • 2011-07-21
  • 2011-05-30
相关资源
最近更新 更多