首先,任何快速解决方案都必须使用矢量化来一次比较多个元素。
但是,到目前为止发布的所有矢量化实现都存在一个共同问题:它们都有分支。结果,他们必须引入数组的块式处理(以减少分支的开销),这导致小型数组的性能低下。对于大型数组,线性搜索比优化好的二分搜索更糟糕,所以优化它没有意义。
然而,线性搜索完全可以在没有分支的情况下实现。这个想法很简单:您想要的索引正是数组中小于您搜索的键的元素数。因此,您可以将数组的每个元素与键值进行比较并对所有标志求和:
static int linear_stgatilov_scalar (const int *arr, int n, int key) {
int cnt = 0;
for (int i = 0; i < n; i++)
cnt += (arr[i] < key);
return cnt;
}
这个解决方案的一个有趣之处在于,即使你对数组进行洗牌,它也会返回相同的答案 =) 虽然这个解决方案似乎相当慢,但它可以优雅地矢量化。下面提供的实现要求数组是 16 字节对齐的。此外,数组必须用 INT_MAX 元素填充,因为它一次消耗 16 个元素。
static int linear_stgatilov_vec (const int *arr, int n, int key) {
assert(size_t(arr) % 16 == 0);
__m128i vkey = _mm_set1_epi32(key);
__m128i cnt = _mm_setzero_si128();
for (int i = 0; i < n; i += 16) {
__m128i mask0 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+0]), vkey);
__m128i mask1 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+4]), vkey);
__m128i mask2 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+8]), vkey);
__m128i mask3 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+12]), vkey);
__m128i sum = _mm_add_epi32(_mm_add_epi32(mask0, mask1), _mm_add_epi32(mask2, mask3));
cnt = _mm_sub_epi32(cnt, sum);
}
cnt = _mm_hadd_epi32(cnt, cnt);
cnt = _mm_hadd_epi32(cnt, cnt);
// int ans = _mm_extract_epi32(cnt, 0); //SSE4.1
int ans = _mm_extract_epi16(cnt, 0); //correct only for n < 32K
return ans;
}
只有在必要时才可以使用 SSE2 实现单个 SSE2 寄存器的最终缩减,它应该不会真正影响整体性能。
我已经在 Intel Core2 Duo E4700 上使用 Visual C++ 2013 x64 编译器对其进行了测试(很旧,是的)。使用 rand() 提供的元素生成大小为 197 的数组。包含所有测试的完整代码在这里。这是执行 32M 搜索的时间:
[OP]
Time = 3.155 (-896368640) //the original OP's code
[Paul R]
Time = 2.933 (-896368640)
[stgatilov]
Time = 1.139 (-896368640) //the code suggested
OP 的原始代码每秒处理 1060 万个数组(每秒 21 亿个元素)。建议的代码每秒处理 2950 万个数组(每秒 58 亿个元素)。此外,建议的代码适用于较小的数组:即使对于 15 个元素的数组,它仍然比 OP 的原始代码快近三倍。
这是生成的程序集:
$LL56@main:
movdqa xmm2, xmm4
movdqa xmm0, xmm4
movdqa xmm1, xmm4
lea rcx, QWORD PTR [rcx+64]
pcmpgtd xmm0, XMMWORD PTR [rcx-80]
pcmpgtd xmm2, XMMWORD PTR [rcx-96]
pcmpgtd xmm1, XMMWORD PTR [rcx-48]
paddd xmm2, xmm0
movdqa xmm0, xmm4
pcmpgtd xmm0, XMMWORD PTR [rcx-64]
paddd xmm1, xmm0
paddd xmm2, xmm1
psubd xmm3, xmm2
dec r8
jne SHORT $LL56@main
$LN54@main:
phaddd xmm3, xmm3
inc rdx
phaddd xmm3, xmm3
pextrw eax, xmm3, 0
最后,我想指出,只要间隔变小,就可以通过切换到所描述的向量化线性搜索来更快地进行优化的二分搜索。
更新:更多信息可以在我的博客文章中找到。