Самый быстрый способ расширить биты в поле на все (перекрывающиеся + смежные) биты в маске?

Скажем, у меня есть 2 двоичных входа с именами IN и MASK. Фактический размер поля может составлять от 32 до 256 бит в зависимости от того, какой набор команд используется для выполнения задачи. Оба входа меняют каждый звонок.

Inputs:
IN   = ...1100010010010100...
MASK = ...0001111010111011...
Output:
OUT  = ...0001111010111000...

редактировать: еще один пример результата из обсуждения некоторых комментариев

IN   = ...11111110011010110...
MASK = ...01011011001111110...
Output:
OUT  = ...01011011001111110...

Я хочу получить смежные смежные 1 бит MASK, внутри которых находится 1 бит IN. (Есть ли общий термин для такого рода операций? Может быть, я неправильно формулирую свои поиски.) Я пытаюсь найти способ сделать это немного быстрее. Я открыт для использования любых расширений SIMD x86 или x86, которые могут сделать это за минимальные циклы ЦП. Предпочтителен более широкий тип данных SIMD, поскольку он позволяет обрабатывать больше данных одновременно.

Лучшее наивное решение, которое я придумал, это следующий псевдокод, который вручную сдвигает влево до тех пор, пока не останется больше совпадающих битов, а затем повторяет сдвиг вправо:

// (using the variables above)
testL = testR = OUT = (IN & MASK);

LoopL:
testL = (testL << 1) & MASK;
if (testL != 0) {
    OUT = OUT | testL;
    goto LoopL;
}

LoopR:
testR = (testR >> 1) & MASK;
if (testR != 0) {
    OUT = OUT | testR;
    goto LoopR;
}

return OUT;

Ответы

Ответ 1

В следующем подходе необходим только один цикл с числом итераций, равным количеству найденных "групп". Я не знаю, будет ли это более эффективным, чем ваш подход; там 6 арифметических/побитовых операций в каждой итерации.

В псевдокоде (C-like):

OUT = 0;
a = MASK;
while (a)
{
    e = a & ~(a + (a & (-a)));
    if (e & IN) OUT |= e;
    a ^= e;
}

Вот как это работает, шаг за шагом, используя 11010111 в качестве примера маски:

OUT = 0

a = MASK        11010111
c = a & (-a)    00000001   keeps rightmost one only
d = a + c       11011000   clears rightmost group (and set the bit to its immediate left)
e = a & ~d      00000111   keeps rightmost group only

if (e & IN) OUT |= e;      adds group to OUT

a = a ^ e       11010000   clears rightmost group, so we can proceed with the next group
c = a & (-a)    00010000
d = a + c       11100000
e = a & ~d      00010000

if (e & IN) OUT |= e;

a = a ^ e       11000000
c = a & (-a)    01000000
d = a + c       00000000   (ignoring carry when adding)
e = a & ~d      11000000

if (e & IN) OUT |= e;

a = a ^ e       00000000   done

Как указано @PeterCordes, некоторые операции можно оптимизировать с помощью инструкций x86 BMI1:

Этот подход хорош для процессорных архитектур, которые не поддерживают побитовое обращение. На архитектурах, которые имеют специальную инструкцию для изменения порядка битов в целом числе, wim answer является более эффективным.

Ответ 2

Я думаю, @fuz комментарий был на правильном пути. В следующем примере показано, как работает приведенный ниже код SSE. Алгоритм начинается с IN_reduced = IN & MASK потому что нас не интересуют биты IN в позициях, где MASK равен 0.

IN                                  = . . . 0 0 0 0 . . . . p q r s . . .
MASK                                = . . 0 1 1 1 1 0 . . 0 1 1 1 1 0 . . 
IN_reduced = IN & MASK              = . . 0 0 0 0 0 0 . . 0 p q r s 0 . .

Если какой-либо из битов pqrs равен 1, тогда IN_reduced + MASK имеет бит переноса 1 в позиции X, который слева направо от запрошенных смежных битов.

MASK                                = . . 0 1 1 1 1 0 . . 0 1 1 1 1 0 . . 
IN_reduced                          = . . 0 0 0 0 0 0 . . 0 p q r s 0 . .
IN_reduced + MASK                   = . . 0 1 1 1 1 . . . 1 . . . . . .
                                                          X
(IN_reduced + MASK) >>1             = . . . 0 1 1 1 1 . . . 1 . . . . . .

При >> 1 этот бит переноса 1 смещается в тот же столбец, что и бит p (первый бит смежных битов). Теперь (IN_reduced + MASK) >>1 на самом деле является средним значением IN_reduced и MASK. Во избежание возможного переполнения сложения мы используем следующее среднее значение: avg(a, b) = (a & b) + ((a ^ b) >> 1) (см. Комментарий @Harold, см. Также здесь и здесь.) При average = avg(IN_reduced, MASK) мы получаем

MASK                                = . . 0 1 1 1 1 0 . . 0 1 1 1 1 0 . . 
IN_reduced                          = . . 0 0 0 0 0 0 . . 0 p q r s 0 . .
average                             = . . . 0 1 1 1 1 . . . 1 . . . . . .
MASK >> 1                           = . . . 0 1 1 1 1 0 . . 0 1 1 1 1 0 .  
leading_bits = (~(MASK>>1))&average = . . . 0 0 0 0 0 . . . 1 0 0 0 0 . .  

Мы можем выделить ведущие биты переноса с помощью leading_bits = (~(MASK>>1) ) & average поскольку MASK>>1 равно нулю в позициях интересующих нас битов переноса.

При обычном сложении перенос распространяется справа налево. Здесь мы используем обратное дополнение: с переносом слева направо. Обратное добавление MASK и leading_bits: rev_added = bit_swap(bit_swap(MASK) + bit_swap(leading_bits)), Это rev_added = bit_swap(bit_swap(MASK) + bit_swap(leading_bits)) биты в требуемых позициях. С OUT = (~rev_added) & MASK мы получаем результат.

MASK                                = . . 0 1 1 1 1 0 . . 0 1 1 1 1 0 . . 
leading_bits                        = . . . 0 0 0 0 0 . . . 1 0 0 0 0 . .  
rev_added (MASK,leading_bits)       = . . . 1 1 1 1 0 . . . 0 0 0 0 1 . .
OUT = ~rev_added & MASK             = . . 0 0 0 0 0 0 . . . 1 1 1 1 0 . .

Алгоритм не был тщательно протестирован, но результат выглядит хорошо.


Алгоритм SSE работает на 2 х 64-битных элементах. Очевидно, что преобразовать его в версию AVX2 с 4-х 64-битными элементами тривиально.

В gcc 9.1 алгоритм компилирует около 29 инструкций, кроме 4 vmovdqa для загрузки некоторых констант, которые, вероятно, выведены из цикла в реальном приложении (после встраивания). Эти 29 инструкций представляют собой хорошее сочетание 9 перемешиваний (vpshufb), которые выполняются на порту 5 (p5) на Intel Skylake, и многих других инструкций, которые часто могут выполняться на p0, p1 или p5.

Следовательно, возможно выполнение около 3 команд за цикл. В этом случае пропускная способность будет примерно 1 вызовом функции (встроенным) на 10 циклов. В случае AVX2 это означает 4 результата uint64_t OUT примерно за 10 циклов.

Обратите внимание, что производительность не зависит от данных (!), Что является большим преимуществом этого ответа, я думаю. Решение без ветвлений и без циклов, и не может пострадать от сбоя прогнозирования ветвлений.


/*  gcc -O3 -m64 -Wall -march=skylake select_bits.c    */
#include <immintrin.h>
#include <stdio.h>
#include <stdint.h>

int print_sse_128_bin(__m128i x);
__m128i bit_128_k(unsigned int k);
__m128i mm_bitreverse_epi64(__m128i x);
__m128i mm_revadd_epi64(__m128i x, __m128i y);


/* Select specific pieces of contiguous bits from 'MASK' based on selector 'IN'  */
__m128i mm_select_bits_epi64(__m128i IN, __m128i MASK){
    __m128i IN_reduced   = _mm_and_si128(IN, MASK);
    /* Compute the average of IN_reduced and MASK with avg(a,b)=(a&b)+((a^b)>>1)  */
    /* (IN_reduced & MASK) + ((IN_reduced ^ MASK) >>1) =                          */
    /* ((IN & MASK) & MASK) + ((IN_reduced ^ MASK) >>1) =                         */
    /* IN_reduced + ((IN_reduced ^ MASK) >>1)                                     */
    __m128i tmp          = _mm_xor_si128(IN_reduced, MASK);
    __m128i tmp_div2     = _mm_srli_epi64(tmp, 1);
    __m128i average      = _mm_add_epi64(IN_reduced, tmp_div2);   /* average is the average */
    __m128i MASK_div2    = _mm_srli_epi64(MASK, 1);
    __m128i leading_bits = _mm_andnot_si128(MASK_div2, average);
    __m128i rev_added    = mm_revadd_epi64(MASK, leading_bits);
    __m128i OUT          = _mm_andnot_si128(rev_added, MASK);
    /* Uncomment the next lines to check the arithmetic */ /*   
    printf("IN           ");print_sse_128_bin(IN           );       
    printf("MASK         ");print_sse_128_bin(MASK         ); 
    printf("IN_reduced   ");print_sse_128_bin(IN_reduced   );       
    printf("tmp          ");print_sse_128_bin(tmp          );       
    printf("tmp_div2     ");print_sse_128_bin(tmp_div2     );       
    printf("average      ");print_sse_128_bin(average      );       
    printf("MASK_div2    ");print_sse_128_bin(MASK_div2    );       
    printf("leading_bits ");print_sse_128_bin(leading_bits );       
    printf("rev_added    ");print_sse_128_bin(rev_added    );       
    printf("OUT          ");print_sse_128_bin(OUT          );       
    printf("\n");*/
    return OUT;       
}


int main(){
    __m128i IN   = _mm_set_epi64x(0b11111110011010110, 0b1100010010010100);
    __m128i MASK = _mm_set_epi64x(0b01011011001111110, 0b0001111010111011);
    __m128i OUT;    

    printf("Example 1 \n");
    OUT = mm_select_bits_epi64(IN, MASK);
    printf("IN           ");print_sse_128_bin(IN);
    printf("MASK         ");print_sse_128_bin(MASK);
    printf("OUT          ");print_sse_128_bin(OUT);
    printf("\n\n");

                      /*  0b7654321076543210765432107654321076543210765432107654321076543210  */
    IN   = _mm_set_epi64x(0b1000001001001010000010000000100000010000000000100000000111100011, 
                          0b11111110011010111);
    MASK = _mm_set_epi64x(0b1110011110101110111111000000000111011111101101111100011111000001, 
                          0b01011011001111111);

    printf("Example 2 \n");
    OUT = mm_select_bits_epi64(IN, MASK);
    printf("IN           ");print_sse_128_bin(IN);
    printf("MASK         ");print_sse_128_bin(MASK);
    printf("OUT          ");print_sse_128_bin(OUT);
    printf("\n\n");

    return 0;
}


int print_sse_128_bin(__m128i x){
    for (int i = 127; i >= 0; i--){
        printf("%1u", _mm_testnzc_si128(bit_128_k(i), x));
        if (((i & 7) == 0) && (i > 0)) printf(" ");
    }
    printf("\n");
    return 0;
}


/* From my answer here https://stackoverflow.com/a/39595704/2439725, adapted to 128-bit */
inline __m128i bit_128_k(unsigned int k){
  __m128i  indices     = _mm_set_epi32(96, 64, 32, 0);
  __m128i  one         = _mm_set1_epi32(1);

  __m128i  kvec        = _mm_set1_epi32(k);  
  __m128i  shiftcounts = _mm_sub_epi32(kvec, indices);
  __m128i  kbit        = _mm_sllv_epi32(one, shiftcounts);   
  return kbit;                             
}


/* Copied from Harold answer https://stackoverflow.com/a/46318399/2439725         */
/* Adapted to epi64 and __m128i: bit reverse two 64 bit elements                    */
inline __m128i mm_bitreverse_epi64(__m128i x){
    __m128i shufbytes = _mm_setr_epi8(7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8); 
    __m128i luthigh = _mm_setr_epi8(0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15);
    __m128i lutlow = _mm_slli_epi16(luthigh, 4);
    __m128i lowmask = _mm_set1_epi8(15);
    __m128i rbytes = _mm_shuffle_epi8(x, shufbytes);
    __m128i high = _mm_shuffle_epi8(lutlow, _mm_and_si128(rbytes, lowmask));
    __m128i low = _mm_shuffle_epi8(luthigh, _mm_and_si128(_mm_srli_epi16(rbytes, 4), lowmask));
    return _mm_or_si128(low, high);
}


/* Add in the reverse direction: With a carry from left to */
/* right, instead of right to left                         */
inline __m128i mm_revadd_epi64(__m128i x, __m128i y){
    x = mm_bitreverse_epi64(x);
    y = mm_bitreverse_epi64(y);
    __m128i sum = _mm_add_epi64(x, y);
    return mm_bitreverse_epi64(sum);
}


Вывод с некомментированным разделом отладки:

Example 1 
IN           00000000 00000000 00000000 00000000 00000000 00000001 11111100 11010110 00000000 00000000 00000000 00000000 00000000 00000000 11000100 10010100
MASK         00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111110 00000000 00000000 00000000 00000000 00000000 00000000 00011110 10111011
IN_reduced   00000000 00000000 00000000 00000000 00000000 00000000 10110100 01010110 00000000 00000000 00000000 00000000 00000000 00000000 00000100 10010000
tmp          00000000 00000000 00000000 00000000 00000000 00000000 00000010 00101000 00000000 00000000 00000000 00000000 00000000 00000000 00011010 00101011
tmp_div2     00000000 00000000 00000000 00000000 00000000 00000000 00000001 00010100 00000000 00000000 00000000 00000000 00000000 00000000 00001101 00010101
average      00000000 00000000 00000000 00000000 00000000 00000000 10110101 01101010 00000000 00000000 00000000 00000000 00000000 00000000 00010001 10100101
MASK_div2    00000000 00000000 00000000 00000000 00000000 00000000 01011011 00111111 00000000 00000000 00000000 00000000 00000000 00000000 00001111 01011101
leading_bits 00000000 00000000 00000000 00000000 00000000 00000000 10100100 01000000 00000000 00000000 00000000 00000000 00000000 00000000 00010000 10100000
rev_added    00000000 00000000 00000000 00000000 00000000 00000000 01001001 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000001 01000111
OUT          00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111110 00000000 00000000 00000000 00000000 00000000 00000000 00011110 10111000

IN           00000000 00000000 00000000 00000000 00000000 00000001 11111100 11010110 00000000 00000000 00000000 00000000 00000000 00000000 11000100 10010100
MASK         00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111110 00000000 00000000 00000000 00000000 00000000 00000000 00011110 10111011
OUT          00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111110 00000000 00000000 00000000 00000000 00000000 00000000 00011110 10111000


Example 2 
IN           10000010 01001010 00001000 00001000 00010000 00000010 00000001 11100011 00000000 00000000 00000000 00000000 00000000 00000001 11111100 11010111
MASK         11100111 10101110 11111100 00000001 11011111 10110111 11000111 11000001 00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111111
IN_reduced   10000010 00001010 00001000 00000000 00010000 00000010 00000001 11000001 00000000 00000000 00000000 00000000 00000000 00000000 10110100 01010111
tmp          01100101 10100100 11110100 00000001 11001111 10110101 11000110 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000010 00101000
tmp_div2     00110010 11010010 01111010 00000000 11100111 11011010 11100011 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 00010100
average      10110100 11011100 10000010 00000000 11110111 11011100 11100100 11000001 00000000 00000000 00000000 00000000 00000000 00000000 10110101 01101011
MASK_div2    01110011 11010111 01111110 00000000 11101111 11011011 11100011 11100000 00000000 00000000 00000000 00000000 00000000 00000000 01011011 00111111
leading_bits 10000100 00001000 10000000 00000000 00010000 00000100 00000100 00000001 00000000 00000000 00000000 00000000 00000000 00000000 10100100 01000000
rev_added    00010000 01100001 00000010 00000001 11000000 01110000 00100000 00100000 00000000 00000000 00000000 00000000 00000000 00000000 01001001 00000000
OUT          11100111 10001110 11111100 00000000 00011111 10000111 11000111 11000001 00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111111

IN           10000010 01001010 00001000 00001000 00010000 00000010 00000001 11100011 00000000 00000000 00000000 00000000 00000000 00000001 11111100 11010111
MASK         11100111 10101110 11111100 00000001 11011111 10110111 11000111 11000001 00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111111
OUT          11100111 10001110 11111100 00000000 00011111 10000111 11000111 11000001 00000000 00000000 00000000 00000000 00000000 00000000 10110110 01111111

Ответ 3

Спасибо всем участникам, которые ответили с решениями до сих пор. Сейчас это мой проект свободного времени, поэтому мне нужно несколько дней, чтобы оценить все варианты. Данный алгоритм является частью внутренней "горячей циклы" векторизованного алгоритма поиска пути. Я собираюсь сделать набор тестов для повторного использования, который я смогу снова запустить на разных микроархитектурах. Я собираюсь сравнить как горячий цикл, так и полный путь, используя его. Я получил 32-битную версию, работающую прошлой ночью, но мне нужно несколько дней, чтобы интегрировать все комбинации (алгоритм bit width x). Я опубликую здесь с результатами, когда они у меня будут.