Многосменная операция

Как реализовать без цикла операцию над битовыми масками, которая для двух битовых масок a а также b ширины n дает битовую маску c ширины 2 * n со следующими свойствами:

  • i-й бит в c устанавливается только при наличии j-й бит в a а также k-й бит в b а также j + k == i

Реализация C ++:

#include <bitset>
#include <algorithm>
#include <iostream>

#include <cstdint>
#include <cassert>

#include <x86intrin.h>

std::uint64_t multishift(std::uint32_t a, std::uint32_t b)
{
std::uint64_t c = 0;
if (_popcnt32(b) < _popcnt32(a)) {
std::swap(a, b);
}
assert(a != 0);
do {
c |= std::uint64_t{b} << (_bit_scan_forward(a) + 1);
} while ((a &= (a - 1)) != 0); // clear least set bit
return c;
}

int main()
{
std::cout << std::bitset< 64 >(multishift(0b1001, 0b0101)) << std::endl; // ...0001011010
}

Может ли оно быть переопределено без цикла, используя некоторые хитрости или инструкции x86?

4

Решение

Это похоже на умножение, которое использует ИЛИ вместо ДОБАВИТЬ. Насколько я знаю, нет действительно удивительного трюка. Но вот трюк, который на самом деле позволяет избегать внутренние, а не их использование:

while (a) {
c |= b * (a & -a);
a &= a - 1;
}

Это очень похоже на ваш алгоритм, но использует умножение для сдвига b оставил после нуля отсчет a, a & -a это хитрость, чтобы выбрать только самый низкий установленный бит в качестве маски. В качестве бонуса это выражение безопасно выполнить, когда a == 0, так что вы можете развернуть (и / или превратить время в do / while без предусловия) без появления неприятных крайних случаев (что не относится к TZCNT и shift).


pshufb может быть использован в режиме параллельного просмотра таблиц, используя a выбрать вложенную таблицу, а затем использовать это, чтобы умножить все кусочки b этим клевом a в одной инструкции. Для умножения собственно, это 8 pshufbs max (или всегда 8, так как меньше попыток раннего выхода с этим). Это требует некоторой странной настройки в начале и некоторых неудачных горизонтальных вещей, чтобы закончить это, так что это может быть не так здорово.

6

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

Поскольку этот вопрос помечен BMI (Intel Haswell и новее, AMD Excavator и новее), я предполагаю, что
AVX2 также представляет интерес здесь. Обратите внимание, что все процессоры с поддержкой BMI2 также имеют поддержку AVX2.
На современном оборудовании, таком как Intel Skylake, используется грубый метод AVX2
может работать довольно хорошо по сравнению со скалярным циклом (do / while), если только a или же b только очень мало битов установлено.
Код ниже вычисляет все 32 умножения (см. Гарольда идея умножения)
без поиска ненулевых битов на входе. После умножения все ИЛИ-е вместе.

При случайных входных значениях aскалярные коды (см. выше: вопрос Ориента или ответ Гарольда)
может пострадать из-за неправильного прогноза ветвления: количество инструкций за цикл (IPC) меньше, чем IPC
решения AVX2 без ответвлений. Я сделал несколько тестов с разными кодами. Оказывается, что
Код Гарольда может значительно выиграть от развертывания цикла.
В этих тестах измеряется пропускная способность, а не задержка. Рассматриваются два случая:
1. Случайный a в среднем установлено 3,5 бита. 2. Случайный a в среднем установлено 16,0 бит (50%).
Процессор Intel Skylake i5-6500.

Time in sec.
3.5 nonz      16 nonz
mult_shft_Orient()             0.81          1.51
mult_shft_Harold()             0.84          1.51
mult_shft_Harold_unroll2()     0.64          1.58
mult_shft_Harold_unroll4()     0.48          1.34
mult_shft_AVX2()               0.44          0.40

Обратите внимание, что эти сроки включают генерацию случайных чисел ввода a а также b,
Код AVX2 выигрывает от быстрого vpmuludq инструкция: имеет пропускную способность
2 за цикл на Скайлэйке.


Код:

/*      gcc -Wall -m64 -O3 -march=broadwell mult_shft.c    */
#include <stdint.h>
#include <stdio.h>
#include <x86intrin.h>

uint64_t mult_shft_AVX2(uint32_t a_32, uint32_t b_32) {

__m256i  a          = _mm256_broadcastq_epi64(_mm_cvtsi32_si128(a_32));
__m256i  b          = _mm256_broadcastq_epi64(_mm_cvtsi32_si128(b_32));
//  0xFEDCBA9876543210  0xFEDCBA9876543210  0xFEDCBA9876543210  0xFEDCBA9876543210
__m256i  b_0        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000000000008, 0x0000000000000004, 0x0000000000000002, 0x0000000000000001));
__m256i  b_1        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000000000080, 0x0000000000000040, 0x0000000000000020, 0x0000000000000010));
__m256i  b_2        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000000000800, 0x0000000000000400, 0x0000000000000200, 0x0000000000000100));
__m256i  b_3        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000000008000, 0x0000000000004000, 0x0000000000002000, 0x0000000000001000));
__m256i  b_4        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000000080000, 0x0000000000040000, 0x0000000000020000, 0x0000000000010000));
__m256i  b_5        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000000800000, 0x0000000000400000, 0x0000000000200000, 0x0000000000100000));
__m256i  b_6        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000008000000, 0x0000000004000000, 0x0000000002000000, 0x0000000001000000));
__m256i  b_7        = _mm256_and_si256(b,_mm256_set_epi64x(0x0000000080000000, 0x0000000040000000, 0x0000000020000000, 0x0000000010000000));

__m256i  m_0        = _mm256_mul_epu32(a, b_0);
__m256i  m_1        = _mm256_mul_epu32(a, b_1);
__m256i  m_2        = _mm256_mul_epu32(a, b_2);
__m256i  m_3        = _mm256_mul_epu32(a, b_3);
__m256i  m_4        = _mm256_mul_epu32(a, b_4);
__m256i  m_5        = _mm256_mul_epu32(a, b_5);
__m256i  m_6        = _mm256_mul_epu32(a, b_6);
__m256i  m_7        = _mm256_mul_epu32(a, b_7);

m_0        = _mm256_or_si256(m_0, m_1);
m_2        = _mm256_or_si256(m_2, m_3);
m_4        = _mm256_or_si256(m_4, m_5);
m_6        = _mm256_or_si256(m_6, m_7);
m_0        = _mm256_or_si256(m_0, m_2);
m_4        = _mm256_or_si256(m_4, m_6);
m_0        = _mm256_or_si256(m_0, m_4);

__m128i  m_0_lo     = _mm256_castsi256_si128(m_0);
__m128i  m_0_hi     = _mm256_extracti128_si256(m_0, 1);
__m128i  e          = _mm_or_si128(m_0_lo, m_0_hi);
__m128i  e_hi       = _mm_unpackhi_epi64(e, e);
e          = _mm_or_si128(e, e_hi);
uint64_t c          = _mm_cvtsi128_si64x(e);
return c;
}uint64_t mult_shft_Orient(uint32_t a, uint32_t b) {
uint64_t c = 0;
do {
c |= ((uint64_t)b) << (_bit_scan_forward(a) );
} while ((a = a & (a - 1)) != 0);
return c;
}uint64_t mult_shft_Harold(uint32_t a_32, uint32_t b_32) {
uint64_t c = 0;
uint64_t a = a_32;
uint64_t b = b_32;
while (a) {
c |= b * (a & -a);
a &= a - 1;
}
return c;
}uint64_t mult_shft_Harold_unroll2(uint32_t a_32, uint32_t b_32) {
uint64_t c = 0;
uint64_t a = a_32;
uint64_t b = b_32;
while (a) {
c |= b * (a & -a);
a &= a - 1;
c |= b * (a & -a);
a &= a - 1;
}
return c;
}uint64_t mult_shft_Harold_unroll4(uint32_t a_32, uint32_t b_32) {
uint64_t c = 0;
uint64_t a = a_32;
uint64_t b = b_32;
while (a) {
c |= b * (a & -a);
a &= a - 1;
c |= b * (a & -a);
a &= a - 1;
c |= b * (a & -a);
a &= a - 1;
c |= b * (a & -a);
a &= a - 1;
}
return c;
}int main(){

uint32_t a,b;

/*
uint64_t c0, c1, c2, c3, c4;
a = 0x10036011;
b = 0x31000107;

//a = 0x80000001;
//b = 0x80000001;

//a = 0xFFFFFFFF;
//b = 0xFFFFFFFF;

//a = 0x00000001;
//b = 0x00000001;

//a = 0b1001;
//b = 0b0101;

c0 = mult_shft_Orient(a, b);
c1 = mult_shft_Harold(a, b);
c2 = mult_shft_Harold_unroll2(a, b);
c3 = mult_shft_Harold_unroll4(a, b);
c4 = mult_shft_AVX2(a, b);
printf("%016lX \n%016lX     \n%016lX     \n%016lX     \n%016lX \n\n", c0, c1, c2, c3, c4);
*/

uint32_t rnd = 0xA0036011;
uint32_t rnd_old;
uint64_t c;
uint64_t sum = 0;
double popcntsum =0.0;
int i;

for (i=0;i<100000000;i++){
rnd_old = rnd;
rnd = _mm_crc32_u32(rnd, i);      /* simple random generator                                   */
b = rnd;                          /* the actual value of b has no influence on the performance */
a = rnd;                          /* `a` has about 50% nonzero bits                            */

#if 1 == 1                           /* reduce number of set bits from about 16 to 3.5                 */
a = rnd & rnd_old;                                   /* now `a` has about 25 % nonzero bits    */
/*0bFEDCBA9876543210FEDCBA9876543210 */
a = (a & 0b00110000101001000011100010010000) | 1;    /* about 3.5 nonzero bits on average      */
#endif
/*   printf("a = %08X \n", a);                */

//   popcntsum = popcntsum + _mm_popcnt_u32(a);
/*   3.5 nonz       50%   (time in sec.)  */
//   c = mult_shft_Orient(a, b );              /*      0.81          1.51                  */
//   c = mult_shft_Harold(a, b );              /*      0.84          1.51                */
//   c = mult_shft_Harold_unroll2(a, b );      /*      0.64          1.58                */
//   c = mult_shft_Harold_unroll4(a, b );      /*      0.48          1.34                */
c = mult_shft_AVX2(a, b );                /*      0.44          0.40                */
sum = sum + c;
}
printf("sum = %016lX \n\n", sum);

printf("average density = %f bits per uint32_t\n\n", popcntsum/100000000);

return 0;
}

функция mult_shft_AVX2() использует нулевые битовые индексы! Так же, как ответ Гарольда.
Похоже, что в вашем вопросе вы начинаете считать биты с 1 вместо 0.
Вы можете умножить ответ на 2, чтобы получить те же результаты.

3

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