【问题标题】:Fast SSE threshold algorithm快速 SSE 阈值算法
【发布时间】:2014-12-21 14:31:56
【问题描述】:

我正在尝试使用 SSE 提出一个非常快速的阈值算法来替换它:

uint8_t *pSrc, *pDst;

// Assume pSrc and pDst point to valid data

// Handle left edge
*pDst++ = *pSrc++;

// Likeness filter
for (uint32_t k = 2; k < width; k++, pSrc++, pDst++)
    if ((*pDst - *pSrc) * (*pDst - *pSrc) > 100 /*THRESHOLD_SQUARED*/) {
        *pDst = *pSrc;
    }
}

// Handle right edge
*pDst++ = *pSrc++;

到目前为止,我有这个:

const uint8_t THRESHOLD = 10;

__attribute__((aligned (16))) static const uint8_t mask[16] = {
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD,
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD,
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD,
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD
};

__m128i xmm1, xmm3, xmm4, xmm5, xmm6, xmm7, xmm8, xmm9;
xmm1 = _mm_load_si128((__m128i const *)mask);
xmm6 = _mm_setzero_si128();

uint8_t *pSrc, *pDst;

// Assume pSrc and pDst point to valid data

// I have other code with another mask for the first 16 entries

for (uint32_t k = 16; k < (width - 16); k += 16, pSrc += 16, pDst += 16) {
    xmm3 = _mm_load_si128((__m128i const *)pDst);
    xmm4 = _mm_load_si128((__m128i const *)pSrc);
    xmm5 = _mm_unpacklo_epi8(xmm3, xmm6);
    xmm7 = _mm_unpackhi_epi8(xmm3, xmm6);
    xmm8 = _mm_unpacklo_epi8(xmm4, xmm6);
    xmm9 = _mm_unpackhi_epi8(xmm4, xmm6);
    xmm5 = _mm_sub_epi16(xmm5, xmm8);
    xmm7 = _mm_sub_epi16(xmm7, xmm9);
    xmm5 = _mm_abs_epi16(xmm5);
    xmm7 = _mm_abs_epi16(xmm7);
    xmm5 = _mm_packs_epi16(xmm5, xmm7);
    xmm5 = _mm_cmpgt_epi8(xmm5, xmm1);
    xmm3 = _mm_blendv_epi8(xmm3, xmm4, xmm5);
    _mm_store_si128((__m128i *)pDst, xmm3);
}

// I have other code with another mask for the last 16 entries

我有想法使用另一种算法来处理两个值之差的绝对值(主要是留在U8(uchar)空间):

a' = a >> 1;
b' = b >> 1;
diff = (abs(sub(a' - b')) << 1) + ((a ^ b) & 1);

这需要 8 条 SSE 指令,而不是上面的 9 条(不包括编译器生成的任何额外寄存器移动),但我不确定它是否由于依赖延迟而更快。

是否有其他 SSE 专家有更好的建议(最高使用 SSE 4.2)?

更新 1 - 感谢 Yves 的建议!

const uint8_t THRESHOLD = 10;

__attribute__((aligned (16))) static const uint8_t mask[16] = {
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD,
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD,
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD,
    THRESHOLD, THRESHOLD, THRESHOLD, THRESHOLD
};

__m128i xmm1, xmm3, xmm4, xmm5, xmm6, xmm7;
xmm1 = _mm_load_si128((__m128i const *)mask);
xmm6 = _mm_setzero_si128();

uint8_t *pSrc, *pDst;

// Assume pSrc and pDst point to valid data

// I have other code with another mask for the first 16 entries

for (uint32_t k = 16; k < (width - 16); k += 16, pSrc += 16, pDst += 16) {
    xmm3 = _mm_load_si128((__m128i const *)pDst);
    xmm4 = _mm_load_si128((__m128i const *)pSrc);
    xmm5 = _mm_subs_epu8(xmm3, xmm4);
    xmm7 = _mm_subs_epu8(xmm4, xmm3);
    xmm5 = _mm_adds_epu8(xmm5, xmm7);
    xmm5 = _mm_subs_epu8(xmm5, xmm1);
    xmm5 = _mm_cmpeq_epi8(xmm5, xmm6);
    xmm4 = _mm_blendv_epi8(xmm4, xmm3, xmm5);
    _mm_store_si128((__m128i *)pDst, xmm4);
}

// I have other code with another mask for the last 16 entries

【问题讨论】:

  • 为什么将标签从 SSE 切换到矢量化? SSE 应该是您的标签之一。
  • 两个代码 sn-ps 不做同样的事情(C 代码未定义未比较的值,而 SSE 代码将它们设置为零)。此外,几乎可以肯定,内存带宽受限于 SSE 阈值这样简单的操作,因此无论如何不要期望算法之间有太大差异。
  • @Damon,多亏了混合指令,Dst 值得以保留。
  • 让我们假设 pSrc 和 pDst 都在一个足够高的缓存中,内存带宽不是一个大问题。
  • 顺便说一句,我检查了 SSE 代码的正确性(新旧与 C 代码)。它们都产生相同的值。

标签: performance algorithm optimization sse simd


【解决方案1】:

Simd 库中有一些有用的函数:

inline __m128i Combine(__m128i mask, __m128i positive, __m128i negative)
{
    return _mm_or_si128(_mm_and_si128(mask, positive), _mm_andnot_si128(mask, negative));
}

inline __m128i AbsDifferenceU8(__m128i a, __m128i b)
{
    return _mm_sub_epi8(_mm_max_epu8(a, b), _mm_min_epu8(a, b));
}

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

所以 SSE2 优化将如下所示:

__m128i t = _mm_set1_epi8(threshold);
for (uint32_t k = 16; k < width - 16; pSrc += 16, pDst += 16)
{
    __m128i src = _mm_load_si128((__m128i*)pSrc);
    __m128i dst = _mm_load_si128((__m128i*)pDst);
    __m128i mask = LesserOrEqual8u(AbsDifferenceU8(src, dst), t);
    _mm_strore_si128((__m128i*)pDst, Combine(mask, dst, src);
}

【讨论】:

  • 这是一个有趣的选择。我想知道它是否有更好的ILP?接受的答案使用三个加/减。所以它受添加吞吐量的约束。大多数英特尔 CPU 可以执行一个 SIMD 添加/时钟周期。您的解决方案使用一个添加和两个最小/最大。假设 min/max 可以使用另一个端口,那么这将比仅使用 add/sub 具有更高的吞吐量。
  • _mm_cmpgt_epu8 不是一个单一的指令命令。它需要额外的 _mm_min_epu8 或 _mm_subs_epu8。我已经尝试过组合方法和混合指令。我没有看到太大的区别(如上所述)。
【解决方案2】:

有一种有效的替代方法可以利用算术饱和度来计算绝对差。

确实,饱和减法计算A - B = Max(A - B, 0),所以|A-B| = (A - B) + (B - A)

Diff= _mm_adds_epu8(_mm_subs_epu8(A, B), _mm_subs_epu8(B, A));

总和不会饱和。这样,您可以保持 16 x 8 位无符号并获得最大吞吐量。

【讨论】:

  • 谢谢! 10 分钟前,我在淋浴时也有类似的想法。这似乎正是我所追求的。我会尝试一下。一旦我的声望达到 15,我就会投票。
  • 如果可以的话:您不必再担心寄存器分配,编译器会为您做正确的事情,您可以使用有意义的标识符和复杂的表达式。
  • 对算法 -_mm_min_epu8(Diff, 0x7f) 进行额外更改后效果很好。这是必需的,因为 _mm_cmpgt_epi8 使用有符号值。我将在今天晚些时候发布该算法的更新版本。您发布的这种方法看起来可以对其他算法进行额外改进 - 我迫不及待地想尝试一下!
  • 对于无符号比较,除了钳制在 0x7f 下,这会缩小范围,您可以使用饱和减法(再次为 _mm_subs_epu8),然后测试 0(_mm_cmpeq_epi8 与预加载的零参数)。
  • 还需要颠倒 _mm_blendv_epi8 - b = _mm_blendv_epi8(b, a, mask) 的操作数顺序,并将 b 移动到目的地。
猜你喜欢
  • 2011-04-11
  • 2013-03-11
  • 2017-07-25
  • 2015-12-01
  • 2013-06-09
  • 1970-01-01
  • 2021-11-12
  • 2019-03-19
  • 2015-03-12
相关资源
最近更新 更多