使用SIMD查找两个元素的最大区别

时间:2018-09-24 19:32:35

标签: algorithm optimization simd

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

CREATE TABLE `properties` (
   `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
   `title` varchar(255) NOT NULL,
   `description` text NOT NULL,
   `name` varchar(255),
   `rental_price` decimal(10, 2),
   `sale_price` decimal(10, 2)
);

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

1 个答案:

答案 0 :(得分:4)

您没有指定任何特定的体系结构,因此我将使用英语描述的算法来保持这种基本的体系结构中立。但是它需要SIMD ISA,该ISA可以有效地分支SIMD比较结果以检查通常为真的条件,例如x86,但不是真正的ARM NEON。

这对于NEON来说效果不佳,因为它没有等效的移动掩码,并且SIMD->整数会导致许多ARM微体系结构停顿。


遍历数组时,通常的情况是一个元素或整个元素的SIMD向量不是新的min而不是{{1} }候选人。我们可以快速浏览这些元素,只有在有了新的diff时才放慢脚步,以获取正确的细节。就像SIMD min或SIMD strlen一样,除了不是停在第一个搜索命中之外,我们只是将标量移动一个块然后恢复。


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

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

    因此,请使用标量代码处理此向量(更新标量min,该标量无需与向量diff同步,因为我们不需要位置)。

    完成后,将最后的diffmax广播到min。或者执行SIMD水平vmin,这样就可以开始无序执行更高版本的SIMD迭代,而无需等待标量中的min。如果标量代码是无分支的,则应该可以很好地工作,因此标量代码中不会出现导致以后的向量工作被丢弃的错误预测。

    或者,SIMD前缀和类型的事物(实际上是前缀-最小)可以生成vmin,其中每个元素都是该点的最小值。({{ 3}})。您可以始终执行此操作以避免出现任何分支,但是如果新候选人很少见,那就太贵了。不过,在分支很难的ARM NEON上,它仍然可行。

  • 如果没有新的最小值,则 SIMD打包最大值vmin 。 (使用饱和减法,如果您使用无符号最大值来处理整个范围,则不会产生较大的无符号差。)

在循环结束时,执行diffmax[0..7] = max(diffmax[0..7], v[0..7]-vmin)向量的SIMD水平最大值。请注意,由于我们不需要需要最大差异的位置,因此当人们找到新的候选者时,我们不需要更新循环中的所有元素。我们甚至不需要使标量特殊情况diffmax和SIMD diffmax彼此保持同步,只需检查末尾以获取标量和SIMD最大差异的最大值即可。


SIMD的最小值/最大值与水平和基本相同,除了您使用packed-max而不是packed-add。对于x86,请参见parallel prefix (cumulative) sum with SSE

或者在带有SSE4.1的x86(用于16位整数元素)上,vdiffmax / phminposuw可以用于最小或最大,有符号或无符号,并且对输入进行了适当的调整。 _mm_minpos_epu16。您可以将diffmax视为无符号的,因为它是非负的,但是Fastest way to do horizontal float vector sum on x86展示了如何将符号位翻转为将范围符号转换为无符号然后返回。


每次找到新的max = -min(-diffmax)候选者时,我们可能都会得出分支预测错误,否则,我们常常找不到新的min候选者,以至于效率低下。

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


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

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

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

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; } 之间的重叠,则可以用最终的未对齐向量替换标量清理。