Може ли да се оптимизира отчитането на съвпаденията на байтове между два низа с помощта на 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 Това казва документът, но TBH всъщност не вярвам на -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:

  • Допълнете и подравнете буфера до 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
Вместо AND на резултата от 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-family). За проста версия, която има 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, напиши на ръка векторизиран код с предложението на Stephen Canon, където отделно натрупваш 256 стойности, преди да ги намалиш до отделна крайна сума. Това ще отдели част от кода извън вътрешния цикъл. - person hdante; 24.03.2013
comment
Усилията на GCC тук са наистина жалки. Може да успеете да го получите, за да избегнете всички разширяващи се преобразувания, ако използвате неподписан вътрешен акумулатор, но в този момент можете също да напишете някои вътрешни елементи. - person Stephen Canon; 24.03.2013