Во-первых, любое быстрое решение должно использовать векторизацию для одновременного сравнения множества элементов.
Однако все опубликованные до сих пор векторизованные реализации страдают одной общей проблемой: у них есть ответвления. В результате приходится вводить поблочную обработку массива (для уменьшения накладных расходов на ветвление), что приводит к низкой производительности для небольших массивов. Для больших массивов линейный поиск хуже, чем хорошо оптимизированный бинарный поиск, поэтому нет смысла его оптимизировать.
Однако линейный поиск можно реализовать вообще без ветвлений. Идея очень проста: индекс, который вам нужен, — это именно то количество элементов в массиве, которое меньше, чем ключ, который вы ищете. Таким образом, вы можете сравнить каждый элемент массива со значением ключа и суммировать все флаги:
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(). Полный код со всеми тестами находится здесь. Вот время для выполнения 32 миллионов поисков:
[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
Напоследок хотелось бы отметить, что хорошо оптимизированный бинарный поиск можно сделать быстрее, переключившись на описанный векторизованный линейный поиск, как только интервал станет мал.
ОБНОВЛЕНИЕ. Дополнительную информацию можно найти на странице моя запись в блоге по этому поводу.
person
stgatilov
schedule
20.07.2015
[code-golf]
. Если бы он был оставлен с таким тегом, он был бы закрыт, потому что проблемы с кодовым гольфом, которые не являются CW, постоянно закрываются. - person Earlz   schedule 01.05.2010a
из отсортированного массиваA
и выполняем линейный поиск этого элемента в отсортированном массивеB
. Это дает нам [возможно, пустую] последовательность ведущих элементов вB
, которые меньше, чемa
. Мы перемещаем всю эту последовательность на выход, за которой следуетa
. Затем повторяем: берем следующий элементa
изA
... и так далее. Вот и все. - person AnT   schedule 02.05.2010a
изA
, выполнить бинарный поиск вB
, переместить начиная сB
до вывода, перемещайтеa
до вывода, повторяйте. - person AnT   schedule 02.05.2010