まず第一に、高速なソリューションでは、ベクトル化を使用して一度に多くの要素を比較する必要があります。
ただし、これまでに投稿されたすべてのベクトル化された実装には、共通の問題があります。それらには分岐があります。その結果、配列のブロック単位の処理を導入する必要があり (分岐のオーバーヘッドを削減するため)、小さな配列のパフォーマンスが低下します。大規模な配列の場合、線形検索は十分に最適化された二分検索よりも悪いため、最適化しても意味がありません。
ただし、線形検索は分岐なしで実装できます。アイデアは非常に単純です。必要なインデックスは、検索するキーよりも少ない配列内の要素の数です。したがって、配列の各要素をキー値と比較し、すべてのフラグを合計できます。
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 バイトでアラインする必要があります。また、配列は一度に 16 個の要素を消費するため、INT_MAX 要素でパディングする必要があります。
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 コンパイラでテストしました。サイズ 197 の配列は、rand() によって提供される要素で生成されます。すべてのテストを含む完全なコードはこちらです。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 の元のコードは、1 秒あたり 1060 万の配列 (1 秒あたり 21 億要素) を処理します。提案されたコードは、1 秒あたり 2950 万の配列 (1 秒あたり 58 億要素) を処理します。また、提案されたコードは小さな配列でもうまく機能します。15 要素の配列であっても、OP の元のコードよりもほぼ 3 倍高速です。
生成されたアセンブリは次のとおりです。
$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
最後に、十分に最適化された二分探索は、間隔が短くなるとすぐに説明したベクトル化された線形探索に切り替えることで高速化できることに注意してください。
更新:詳細については、この件に関する私のブログ投稿を参照してください。