Можно ли оптимизировать эту функцию с помощью SIMD?

Профилирование говорит о том, что эта функция является настоящей бутылкой для моего приложения:

static inline int countEqualChars(const char* string1, const char* string2, int size) {
int r = 0;
for (int j = 0; j < size; ++j) {
if (string1[j] == string2[j]) {
++r;
}
}

return r;
}

Даже с -O3 и -march = native G ++ 4.7.2 не векторизует эту функцию (я проверял вывод ассемблера). Теперь я не эксперт по SSE и друзьям, но я думаю, что сравнение более чем одного персонажа одновременно должно быть быстрее. Любые идеи о том, как ускорить процесс? Целевая архитектура x86-64.

7

Решение

Флаги компилятора для векторизации:

-ftree-vectorize

-ftree-vectorize-march=<your_architecture>

Использование встроенных функций SSEx:

  • Заполните буфер и выровняйте его до 16 байтов (в соответствии с размером вектора, который вы на самом деле собираетесь использовать)

  • Создать накопитель countU8 с _mm_set1_epi8(0)

  • Для всех n / 16 входных (под) векторов выполните:

    • Загрузите 16 символов из обеих строк _mm_load_si128 или же _mm_loadu_si128 (для не выровненных грузов)

    • _mm_cmpeq_epi8
      сравнить октеты параллельно. Каждый матч дает 0xFF (-1), 0x00 иначе.

    • Вычтите вышеупомянутый вектор результата из countU8 с помощью _mm_sub_epi8 (минус -1 -> +1)

    • Всегда после 255 циклов 16 8-битных счетчиков должны быть извлечены в больший целочисленный тип для предотвращения переполнения. Смотрите как распаковать и добавить по горизонтали в этом хорошем ответе, как это сделать: https://stackoverflow.com/a/10930706/1175253

Код:

#include <iostream>
#include <vector>

#include <cassert>
#include <cstdint>
#include <climits>
#include <cstring>

#include <emmintrin.h>

#ifdef __SSE2__

#if !defined(UINTPTR_MAX) ||  !defined(UINT64_MAX) ||  !defined(UINT32_MAX)
#  error "Limit macros are not defined"#endif

#if UINTPTR_MAX == UINT64_MAX
#define PTR_64
#elif UINTPTR_MAX == UINT32_MAX
#define PTR_32
#else
#  error "Current UINTPTR_MAX is not supported"#endif

template<typename T>
void print_vector(std::ostream& out,const __m128i& vec)
{
static_assert(sizeof(vec) % sizeof(T) == 0,"Invalid element size");
std::cout << '{';
const T* const end   = reinterpret_cast<const T*>(&vec)-1;
const T* const upper = end+(sizeof(vec)/sizeof(T));
for(const T* elem = upper;
elem != end;
--elem
)
{
if(elem != upper)
std::cout << ',';
std::cout << +(*elem);
}
std::cout << '}' << std::endl;
}

#define PRINT_VECTOR(_TYPE,_VEC) do{  std::cout << #_VEC << " : "; print_vector<_TYPE>(std::cout,_VEC);    } while(0)

///@note SSE2 required (macro: __SSE2__)
///@warning Not tested!
size_t counteq_epi8(const __m128i* a_in,const __m128i* b_in,size_t count)
{
assert(a_in != nullptr && (uintptr_t(a_in) % 16) == 0);
assert(b_in != nullptr && (uintptr_t(b_in) % 16) == 0);
//assert(count > 0);/*
//maybe not so good with all that branching and additional loop variables

__m128i accumulatorU8 = _mm_set1_epi8(0);
__m128i sum2xU64 = _mm_set1_epi8(0);
for(size_t i = 0;i < count;++i)
{

//this operation could also be unrolled, where multiple result registers would be accumulated
accumulatorU8 = _mm_sub_epi8(accumulatorU8,_mm_cmpeq_epi8(*a_in++,*b_in++));
if(i % 255 == 0)
{
//before overflow of uint8, the counter will be extracted
__m128i sum2xU16 = _mm_sad_epu8(accumulatorU8,_mm_set1_epi8(0));
sum2xU64 = _mm_add_epi64(sum2xU64,sum2xU16);

//reset accumulatorU8
accumulatorU8 = _mm_set1_epi8(0);
}
}

//blindly accumulate remaining values
__m128i sum2xU16 = _mm_sad_epu8(accumulatorU8,_mm_set1_epi8(0));
sum2xU64 = _mm_add_epi64(sum2xU64,sum2xU16);

//do a horizontal addition of the two counter values
sum2xU64 = _mm_add_epi64(sum2xU64,_mm_srli_si128(sum2xU64,64/8));

#if defined PTR_64
return _mm_cvtsi128_si64(sum2xU64);
#elif defined PTR_32
return _mm_cvtsi128_si32(sum2xU64);
#else
#  error "macro PTR_(32|64) is not set"#endif

*/

__m128i sum2xU64 = _mm_set1_epi32(0);
while(count--)
{
__m128i matches     = _mm_sub_epi8(_mm_set1_epi32(0),_mm_cmpeq_epi8(*a_in++,*b_in++));
__m128i sum2xU16    = _mm_sad_epu8(matches,_mm_set1_epi32(0));
sum2xU64    = _mm_add_epi64(sum2xU64,sum2xU16);
#ifndef NDEBUG
PRINT_VECTOR(uint16_t,sum2xU64);
#endif
}

//do a horizontal addition of the two counter values
sum2xU64 = _mm_add_epi64(sum2xU64,_mm_srli_si128(sum2xU64,64/8));
#ifndef NDEBUG
std::cout << "----------------------------------------" << std::endl;
PRINT_VECTOR(uint16_t,sum2xU64);
#endif

#if !defined(UINTPTR_MAX) ||  !defined(UINT64_MAX) ||  !defined(UINT32_MAX)
#  error "Limit macros are not defined"#endif

#if defined PTR_64
return _mm_cvtsi128_si64(sum2xU64);
#elif defined PTR_32
return _mm_cvtsi128_si32(sum2xU64);
#else
#  error "macro PTR_(32|64) is not set"#endif

}

#endif

int main(int argc, char* argv[])
{

std::vector<__m128i> a(64); // * 16 bytes
std::vector<__m128i> b(a.size());
const size_t nBytes = a.size() * sizeof(std::vector<__m128i>::value_type);

char* const a_out = reinterpret_cast<char*>(a.data());
char* const b_out = reinterpret_cast<char*>(b.data());

memset(a_out,0,nBytes);
memset(b_out,0,nBytes);

a_out[1023] = 1;
b_out[1023] = 1;

size_t equalBytes = counteq_epi8(a.data(),b.data(),a.size());

std::cout << "equalBytes = " << equalBytes << std::endl;

return 0;
}

Самая быстрая реализация SSE, которую я получил для больших и маленьких массивов:

size_t counteq_epi8(const __m128i* a_in,const __m128i* b_in,size_t count)
{
assert((count > 0 ? a_in != nullptr : true) && (uintptr_t(a_in) % sizeof(__m128i)) == 0);
assert((count > 0 ? b_in != nullptr : true) && (uintptr_t(b_in) % sizeof(__m128i)) == 0);
//assert(count > 0);

const size_t maxInnerLoops    = 255;
const size_t nNestedLoops     = count / maxInnerLoops;
const size_t nRemainderLoops  = count % maxInnerLoops;

const __m128i zero  = _mm_setzero_si128();
__m128i sum16xU8    = zero;
__m128i sum2xU64    = zero;

for(size_t i = 0;i < nNestedLoops;++i)
{
for(size_t j = 0;j < maxInnerLoops;++j)
{
sum16xU8 = _mm_sub_epi8(sum16xU8,_mm_cmpeq_epi8(*a_in++,*b_in++));
}
sum2xU64 = _mm_add_epi64(sum2xU64,_mm_sad_epu8(sum16xU8,zero));
sum16xU8 = zero;
}

for(size_t j = 0;j < nRemainderLoops;++j)
{
sum16xU8 = _mm_sub_epi8(sum16xU8,_mm_cmpeq_epi8(*a_in++,*b_in++));
}
sum2xU64 = _mm_add_epi64(sum2xU64,_mm_sad_epu8(sum16xU8,zero));

sum2xU64 = _mm_add_epi64(sum2xU64,_mm_srli_si128(sum2xU64,64/8));

#if UINTPTR_MAX == UINT64_MAX
return _mm_cvtsi128_si64(sum2xU64);
#elif UINTPTR_MAX == UINT32_MAX
return _mm_cvtsi128_si32(sum2xU64);
#else
#  error "macro PTR_(32|64) is not set"#endif
}
6

Другие решения

Конечно может.

pcmpeqb сравнивает два вектора по 16 байтов и создает вектор с нулями, где они различаются, и -1, где они совпадают. Используйте это для сравнения 16 байтов за раз, добавляя результат к вектору-аккумулятору (убедитесь, что вы собрали результаты не более 255 векторов, чтобы избежать переполнения). Когда вы закончите, в аккумуляторе будет 16 результатов. Суммируйте их и отрицайте, чтобы получить количество равных элементов.

Если длины очень короткие, будет трудно добиться значительного ускорения при таком подходе. Если длина длинная, то ее стоит продолжить.

8

Автоматическая векторизация в текущем gcc — это вопрос, помогающий компилятору понять, что код легко векторизовать. В вашем случае: он поймет запрос на векторизацию, если вы удалите условный код и перезапишите код более обязательным образом:

    static inline int count(const char* string1, const char* string2, int size) {
int r = 0;
bool b;

for (int j = 0; j < size; ++j) {
b = (string1[j] == string2[j]);
r += b;
}

return r;
}

В этом случае:

movdqa  16(%rsp), %xmm1
movl    $.LC2, %esi
pxor    %xmm2, %xmm2
movzbl  416(%rsp), %edx
movdqa  .LC1(%rip), %xmm3
pcmpeqb 224(%rsp), %xmm1
cmpb    %dl, 208(%rsp)
movzbl  417(%rsp), %eax
movl    $1, %edi
pand    %xmm3, %xmm1
movdqa  %xmm1, %xmm5
sete    %dl
movdqa  %xmm1, %xmm4
movzbl  %dl, %edx
punpcklbw   %xmm2, %xmm5
punpckhbw   %xmm2, %xmm4
pxor    %xmm1, %xmm1
movdqa  %xmm5, %xmm6
movdqa  %xmm5, %xmm0
movdqa  %xmm4, %xmm5
punpcklwd   %xmm1, %xmm6

(так далее.)

3
По вопросам рекламы [email protected]