我编写了一种算法,以获取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的新手,到目前为止,我还没有成功
答案 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;
}
之间的重叠,则可以用最终的未对齐向量替换标量清理。