Прежде всего, любое быстрое решение должно использовать векторизацию для сравнения сразу нескольких элементов.
Однако все векторизованные реализации, опубликованные до сих пор, страдают от общей проблемы: у них есть ветви. В результате им приходится вводить блочную обработку массива (чтобы уменьшить издержки ветвления), что приводит к низкой производительности для небольших массивов. Для больших массивов линейный поиск хуже, чем хорошо оптимизированный двоичный поиск, поэтому нет смысла его оптимизировать.
Однако линейный поиск может быть реализован вообще без ветвей. Идея очень проста: индекс, который вы хотите, - это количество элементов в массиве, которое меньше, чем ключ, который вы ищете. Таким образом, вы можете сравнить каждый элемент массива со значением ключа и суммировать все флаги:
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, только если это необходимо, оно не должно реально влиять на общую производительность.
Я тестировал его с помощью компилятора Visual C ++ 2013 x64 на Intel Core2 Duo E4700 (довольно старый, да). Массив размером 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 обрабатывает 10,6 миллиона массивов в секунду (2,1 миллиарда элементов в секунду). Предлагаемый код обрабатывает 29,5 миллионов массивов в секунду (5,8 миллиардов элементов в секунду).
Кроме того, предлагаемый код хорошо работает для небольших массивов: даже для массивов из 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
Наконец, я хотел бы отметить, что хорошо оптимизированный двоичный поиск можно сделать быстрее, переключившись на описанный векторизованный линейный поиск, как только интервал станет небольшим.
ОБНОВЛЕНИЕ: Более подробную информацию можно найти в моем блоге по этому вопросу.