Можно ли с помощью 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.


person Milan    schedule 24.03.2013    source источник
comment
На что обычно похожи входы? Какого размера и являются ли они переменными или буквальными строками? Кроме того, в чем причина необходимости этой функции - каково ее более глубокое значение в вашей системе?   -  person John Zwinck    schedule 24.03.2013
comment
вы пробовали использовать флаги -msse и т. д.? и измерять производительность до и после факта? См. другой пример   -  person Petesh    schedule 24.03.2013
comment
Я попробовал -msse и не заметил никакой разницы во времени выполнения. Обе строки гарантированно имеют одинаковую длину. Однако размеры сильно различаются.   -  person Milan    schedule 24.03.2013
comment
@Petesh: OP использовал -march=native, что подразумевает любые -mfoo флаги, поддерживаемые его процессором.   -  person    schedule 24.03.2013
comment
@Fanael Это то, что говорится в документе, но я не верю, что -march=native поступает правильно (это из опыта старых вариантов gcc, сейчас это может быть не так)   -  person Petesh    schedule 24.03.2013
comment
Кстати, вы также можете сделать это быстрее без SSE2 (хотя все процессоры x86_64 поддерживают это), используя методы SWAR. Простейшая форма этого - пропуск побайтовых сравнений, если два 64-битных слова равны. Более сложная форма может создать 64-битное слово, в котором каждый байт равен 1, если соответствующие байты в строках равны, и 0, если нет; эти слова можно обрабатывать как с SSE2.   -  person jilles    schedule 24.03.2013
comment
Now, I'm not an expert with SSE and friends - ›Вы уже дали ключ к разгадке. Мой совет: возьмите вводный материал;) Этот вопрос в основном похож на просьбу о коде, но выражен довольно четко.   -  person Sebastian Mach    schedule 05.04.2013
comment
Как подсчитывать количество символов с помощью SIMD имеет версию AVX2 (с использованием одного символа вместо второй строки, но векторизация выполняется идентично).   -  person Peter Cordes    schedule 02.05.2019


Ответы (3)


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

-ftree-vectorize

-ftree-vectorize -march=<your_architecture> (Используйте все расширения набора инструкций, доступные на вашем компьютере, а не только базовый уровень, такой как SSE2 для x86-64). Используйте -march=native для оптимизации для машины, на которой работает компилятор.) -march=<foo> также устанавливает -mtune=<foo>, что тоже хорошо.

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

  • Padd и выровняйте буфер до 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
}
person Sam    schedule 24.03.2013
comment
Вместо того, чтобы объединять результат pcmpeqb с маской и затем добавлять его в аккумулятор, вы также можете вычесть результат из аккумулятора, сохранив инструкцию в цикле. - person jilles; 24.03.2013
comment
Большое спасибо, это действительно помогло :) - person Milan; 24.03.2013
comment
Вы можете выполнять горизонтальное сложение более эффективно, используя psadbw с нулем, с последующим перемещением 64 бита в низкий и добавлением. - person Stephen Canon; 24.03.2013
comment
Напишем этот цикл? - person hdante; 24.03.2013
comment
Просто добавил код, который у меня есть. Я постарался реализовать все предложенные улучшения. - person Sam; 24.03.2013
comment
Моя реализация: gist.github.com/hdante/5232848 Он использует внутренний цикл 255 итераций. Мне понравился последний внутренний цикл, всего 7 инструкций: gist.github.com/hdante/5232856 - person hdante; 24.03.2013
comment
С другой стороны, я не смог увидеть улучшений по сравнению с версией с автоматической векторизацией. Я протестировал обе реализации, и они на 1% быстрее, чем версия gcc. Боюсь, что цикл останавливается при загрузке памяти. simfoo, вы видите улучшения? - person hdante; 24.03.2013
comment
Я добавил самую быструю из имеющихся у меня реализаций. @hdante: assert(equalBytes == nBytes); не работает с вашей реализацией в моем коде (кажется, что оставшиеся size % something циклы отсутствуют). Что касается нагрузки на память: мы можем попробовать _mm_prefetch() некоторые строки кэша, хотя я считаю, что это существенно влияет только на старые процессоры Pentium (II / III). - person Sam; 25.03.2013
comment
Да, я написал цикл только для кратных 255 векторов для простоты. В любом случае, окончательное улучшение исходного скалярного кода кажется минимальным. - person hdante; 25.03.2013
comment
Как подсчитать количество символов с помощью SIMD имеет версию AVX2, использующую PSADBW вне внутреннего цикла, и несколько аккумуляторов, поэтому она может работать быстрее, чем 1 вектор на тактовая частота без узких мест на 1 цикл psubb задержка (или 2 цикла в семействе AMD Bulldozer). Для простой версии с SAD внутри внутреннего цикла см. Быстрый подсчет количества равных байтов между двумя массивами. Вы можете SAD против вектора 0x7f и вычесть поправку вне цикла, вместо того, чтобы делать его положительным 0-cmp(). - person Peter Cordes; 02.05.2019

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

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

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

person Stephen Canon    schedule 24.03.2013
comment
Спасибо, по крайней мере, теперь я знаю, что это возможно - person Milan; 24.03.2013

Автоматическая векторизация в текущем 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

(и т.д.)

person hdante    schedule 24.03.2013
comment
Я посмотрел на оставшуюся часть разборки, и, скажем так, есть возможности для улучшения. - person harold; 24.03.2013
comment
Только примерно на 5% быстрее с большим набором данных :( Тем не менее, спасибо за предложение - person Milan; 24.03.2013
comment
simfoo, вручную напишите векторизованный код с предложением Стивена Кэнона, где вы отдельно накапливаете 256 значений, прежде чем сводить к отдельной окончательной сумме. Это позволит исключить часть кода из внутреннего цикла. - person hdante; 24.03.2013
comment
Усилия GCC здесь действительно жалкие. Вы могли бы получить его, чтобы избежать всех расширяющихся преобразований, если вы используете внутренний аккумулятор unsigned char, но в этот момент вы могли бы также написать некоторые встроенные функции. - person Stephen Canon; 24.03.2013