линейный поиск через uint64[] с SSE

Я пытаюсь реализовать линейный поиск по массиву uint64, используя инструкции SSE. У меня все работает для uint16 и uint32, но я получаю ошибки компилятора для кода uint64 (linux, gcc - см. спецификации в конце).

Я пытаюсь сравнить 64-битные числа 2x2, а затем каким-то образом перевести результат в индекс для моего массива. Это хорошо работает с uint32 (кредиты идут на http://schani.wordpress.com/2010/04/30/linear-vs-binary-search/):

#include <xmmintrin.h>
#include <smmintrin.h>

typedef ham_u64_t vec2uint64 __attribute__ ((vector_size (16)));
typedef ham_u32_t vec4uint32 __attribute__ ((vector_size (16)));
typedef float     vec4float  __attribute__ ((vector_size (16)));
typedef ham_u16_t vec8uint16 __attribute__ ((vector_size (16)));
typedef ham_u8_t  vec16uint8 __attribute__ ((vector_size (16)));

// ...

vec4uint32 v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
vec4uint32 v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 4]);
vec4uint32 v3 = _mm_loadu_si128((const __m128i *)&data[start + i + 8]);
vec4uint32 v4 = _mm_loadu_si128((const __m128i *)&data[start + i + 12]);

vec4uint32 cmp0 = _mm_cmpeq_epi32(key4, v1);
vec4uint32 cmp1 = _mm_cmpeq_epi32(key4, v2);
vec4uint32 cmp2 = _mm_cmpeq_epi32(key4, v3);
vec4uint32 cmp3 = _mm_cmpeq_epi32(key4, v4);

vec8uint16 pack01 = __builtin_ia32_packssdw128(cmp0, cmp1);
vec8uint16 pack23 = __builtin_ia32_packssdw128(cmp2, cmp3);
vec16uint8 pack0123 = __builtin_ia32_packsswb128(pack01, pack23);

int res = __builtin_ia32_pmovmskb128(pack0123);
if (res > 0) {
  int czt = __builtin_ctz(~res + 1);
  return (start + i + czt);
}

Вот что я придумал для uint64. Сравнение работает, я просто не знаю, что делать с результатами, а вызов __builtin_ia32_packssdw() не компилируется:

vec2uint64 v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
vec2uint64 v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 2]);

vec2uint64 cmp0 = _mm_cmpeq_epi64(key2, v1);
vec2uint64 cmp1 = _mm_cmpeq_epi64(key2, v2);

vec4uint32 pack01 = __builtin_ia32_packssdw(cmp0, cmp1); // error
vec4uint32 pack23 = _mm_set1_epi32(0);
vec16uint8 pack0123 = __builtin_ia32_packsswb128(pack01, pack23);

int res = __builtin_ia32_pmovmskb128(pack0123);
if (res > 0) {
  int czt = __builtin_ctz(~res + 1);
  return (start + i + czt);
}

Ошибка говорит:

error: cannot convert 'vec1uint64 {aka __vector(2) long unsigned int}'
to '__vector(2) int' for argument '1' to '__vector(4) short int
__builtin_ia32_packssdw(__vector(2) int, __vector(2) int)'

(Определения типов для vec2uint64 находятся вверху, в коде для uint32.)

Моя среда:

Linux ws4484 3.5.0-48-generic #72~precise1-Ubuntu SMP Tue Mar 11 20:09:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)

Мой вопрос заключается не только в том, как я могу исправить ошибку компилятора, но если у кого-то есть лучшая идея получить индекс массива с совпадением, может быть, без всей упаковки?

Заранее спасибо!


person cruppstahl    schedule 15.04.2014    source источник
comment
Почему вы используете низкоуровневые встроенные функции, такие как __builtin_ia32_packssdw, вместо более привычных _mm_packs_epi32? Это делает работу с векторными типами намного более запутанной.   -  person Paul R    schedule 15.04.2014
comment
Нет конкретной причины; я не знал о _mm_packs_epi32. Спасибо за комментарий, заменю.   -  person cruppstahl    schedule 15.04.2014
comment
Если вы используете обычные встроенные функции высокого уровня, вы можете просто использовать __m128i для всех ваших векторных переменных и не беспокоиться о смешивании разных типов для 32-битных и 64-битных операций, которые, как вы видели, могут быть уродливыми.   -  person Paul R    schedule 15.04.2014
comment
Я последовал твоему предложению. Спасибо за вашу помощь и ваши комментарии!   -  person cruppstahl    schedule 15.04.2014
comment
Вы пишете на C или на C++?   -  person Lightness Races in Orbit    schedule 10.10.2015
comment
C++, но обычный C тоже подойдет.   -  person cruppstahl    schedule 10.10.2015


Ответы (3)


Я предлагаю НЕ использовать встроенные встроенные функции и неявные векторы. Это имеет смысл только в том случае, если вы не используете встроенные функции, отличные от GCC (например, _mm_cmpeq_epi32), и хотите придерживаться только GCC. Вы можете делать то, что хотите, вот так

__m128i key2 = _mm_set1_epi64x(key);
__m128i v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
__m128i v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 2]);

__m128i cmp0 = _mm_cmpeq_epi64(key2, v1);
__m128i cmp1 = _mm_cmpeq_epi64(key2, v2);

__m128i low2  = _mm_shuffle_epi32(cmp0,0xD8);  
__m128i high2 = _mm_shuffle_epi32(cmp1,0xD8);      
__m128i pack = _mm_unpacklo_epi64(low2,high2);

__m128i pack01 = _mm_packs_epi32(pack, _mm_setzero_si128());
__m128i pack0123 = _mm_packs_epi16(pack01, _mm_setzero_si128());

int res =  _mm_movemask_epi8(pack0123);

Вы, вероятно, можете найти более эффективную версию, которая позволяет избежать упаковки, но тогда вам придется использовать функцию, отличную от __builtin_ctz.

Для 32-битных целых чисел я предлагаю

__m128i key4 = _mm_set1_epi32(key);
__m128i v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
__m128i v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 4]);
__m128i v3 = _mm_loadu_si128((const __m128i *)&data[start + i + 8]);
__m128i v4 = _mm_loadu_si128((const __m128i *)&data[start + i + 12]);

__m128i cmp0 = _mm_cmpeq_epi32(key4, v1);
__m128i cmp1 = _mm_cmpeq_epi32(key4, v2);
__m128i cmp2 = _mm_cmpeq_epi32(key4, v3);
__m128i cmp3 = _mm_cmpeq_epi32(key4, v4);

__m128i pack01 = _mm_packs_epi32(cmp0, cmp1);
__m128i pack23 = _mm_packs_epi32(cmp2, cmp3);
__m128i pack0123 = _mm_packs_epi16(pack01, pack23);

int res = _mm_movemask_epi8(pack0123);
person Z boson    schedule 15.04.2014
comment
Это работает отлично. Также спасибо за 32-битные предложения - я уже заменил встроенные функции на основе предложения @Paul R. - person cruppstahl; 15.04.2014
comment
Я предполагаю, что встроенные встроенные функции GCC имеют и другие преимущества. Например. _mm_cmpeq_epi64 требует SSE4.1 для SSE2, это сложно, и встроенные функции GCC позаботятся об этом за вас. - person Z boson; 15.04.2014
comment
Для больших массивов вы можете ускорить внутренний цикл, если просто ИЛИ объедините результаты cmp, чтобы увидеть, есть ли какое-либо совпадение. Затем вне цикла разберитесь, где было совпадение с перемешиванием/упаковкой. (Это похоже на то, что делает memchr glibc, но даже больше выигрыша, потому что все это перетасовка стоит дорого.) Кроме того, _mm_packs_epi32 с нулями кажется огромной тратой; просто используйте _mm_movemask_ps, чтобы получить старшие биты каждого элемента dword, если вам нужна небольшая битовая маска. - person Peter Cordes; 29.06.2020

Это полное решение для поиска совпадений в 64-битных значениях. В этом случае значение (namehash) является членом структуры. Эта процедура сравнивает 8 64-битных значений на каждой итерации и предоставляет соответствующий индекс структуры.

//ptr is a struct array
__m128i key2 = _mm_set1_epi64x(k); //k is the 64 bit search key
for(;;)
   {
   if(!ptr[i].namehash)return NULL;
   __m128i v1 = _mm_set_epi64x (ptr[i+1].namehash,ptr[i].namehash);
   __m128i v2 = _mm_set_epi64x (ptr[i+3].namehash,ptr[i+2].namehash);
   __m128i v3 = _mm_set_epi64x (ptr[i+5].namehash,ptr[i+4].namehash);
   __m128i v4 = _mm_set_epi64x (ptr[i+7].namehash,ptr[i+6].namehash);

   __m128i cmp0 = _mm_cmpeq_epi64(key2, v1);
   __m128i cmp1 = _mm_cmpeq_epi64(key2, v2);
   __m128i cmp2 = _mm_cmpeq_epi64(key2, v3);
   __m128i cmp3 = _mm_cmpeq_epi64(key2, v4);

   __m128i L0  = _mm_shuffle_epi32(cmp0,0xD8);
   __m128i H1 = _mm_shuffle_epi32(cmp1,0xD8);
   __m128i L2  = _mm_shuffle_epi32(cmp2,0xD8);
   __m128i H3 = _mm_shuffle_epi32(cmp3,0xD8);

   __m128i pack0 = _mm_unpacklo_epi64(L0,H1);
   __m128i pack1 = _mm_unpacklo_epi64(L2,H3);

   __m128i pack01 = _mm_packs_epi32(pack0,pack1);
   __m128i pack0123 = _mm_packs_epi16(pack01, _mm_setzero_si128());

   res =  _mm_movemask_epi8(pack0123);
   if(res > 0)break;
   i+=8;
   }

int index = i + __builtin_ctz(res);  //The struct table index to the matching struct.

ВАЖНО: длина массива структур должна быть кратна 8, по крайней мере, с 1 завершающим элементом NULL.

В качестве альтернативы, если требуется только 2 64-битных сравнения на итерацию, приведенное выше можно значительно упростить до:

for(;;)
   {
   if(!ptr[i].namehash)return NULL;
   __m128i v1 = _mm_set_epi64x (ptr[i+1].namehash,ptr[i].namehash);
   __m128i cmp0 = _mm_cmpeq_epi64(key2, v1);
   res =  _mm_movemask_epi8(cmp0);
   if(res > 0)break;
   i+=2;
   }
int ctz = __builtin_ctz(res);
int index = i + (ctz>>5);  //The struct table index to the matching struct.
person poby    schedule 09.10.2015
comment
Для больших массивов вы можете ускорить внутренний цикл, если просто ИЛИ объедините результаты cmp, чтобы увидеть, есть ли какое-либо совпадение. Затем вне цикла разберитесь, где было совпадение с перемешиванием/упаковкой. (Это похоже на то, что делает memchr glibc, но еще большая победа, потому что вся эта перетасовка стоит дорого.) - person Peter Cordes; 29.06.2020
comment
Кроме того, последний шаг упаковки нулями можно выполнить, используя вместо этого (__builtin_ctz(res)>>1), вне цикла, если вы не хотите оптимизировать цикл до 4x pcmp + 3x por + pmovmskb - person Peter Cordes; 29.06.2020

Я не могу найти никаких инструкций для преобразования 64-битного целого числа в 32-битное целое число, что вам нужно, чтобы затем использовать packssdw и т. д. Это получается довольно долго и запутанно, но должно работать:

Итак, я думаю, что решение состоит в том, чтобы использовать битовую маску (биты 0, 1, 2, 3:

Они идут перед циклом:

vec2uint64 mask0 = { 2, 1 };
vec2uint64 mask1 = { 8, 4 };
vec2uint64 zero  = { 0, 0 };

Внутренняя петля:

vec2uint64 res0 = _mm_and_si128(cmp0, mask0);
vec2uint64 res1 = _mm_and_si128(cmp1, mask1);

vec2uint64 res2 = _mm_or_si128(res0, res1);

Затем нам нужно перетасовать старшую часть в младшую часть новой переменной:

vec2uint64 hi0 = _mm_unpackhi_epi64(res0, zero);
vec2uint64 hi1 = _mm_unpackhi_epi64(res1, zero);

vec2uint64 hi2 = _mm_or_si128(hi0, hi1);
vec2uint64 res3 = _mm_or_si128(res2, hi2);

Теперь младшие 64 бита [ну, младшие 4 бита, остальные равны нулю] res — это битовая маска, одно из значений которой совпадает.

int res = _mm_cvtsi128_si32(res3);

(Теперь вы можете считать конечные нули, как и раньше).

person Mats Petersson    schedule 15.04.2014
comment
Упаковать четыре 64-битных значения из двух регистров SSE в четыре 32-битных значения в одном регистре SSE можно с помощью трех встроенных функций: __m128i low2 = _mm_shuffle_epi32(cmp0,0xD8); __m128i high2 = _mm_shuffle_epi32(cmp1,0xD8); _mm_unpacklo_epi64(low2,high2). - person Z boson; 15.04.2014
comment
Я пробовал эту функцию, и она не давала стабильных результатов для всех моих тестов. Результаты были найдены, но количество нулей в конце больше не соответствовало действительности. Тем не менее, спасибо за ваш ответ, я ценю это. - person cruppstahl; 15.04.2014