Обнаружение совпадающих битов в С++

Я пытаюсь взять два объекта bitset, например

a = 10010111
b = 01110010

и удалите биты из обеих переменных, если они совпадают в одном и том же положении/индексе. Таким образом, мы останемся с

a = 100xx1x1 = 10011
b = 011xx0x0 = 01100

Есть ли способ достичь этого?

Ответ 1

Другие ответы показали хорошие идиоматические C++ способы сделать это. К сожалению, они будут довольно медленными. Даже умное решение на основе шаблонов AndyG, хотя оно выполняет большую часть работы во время компиляции, все же заставляет компилятор генерировать много кода, который должен выполняться во время выполнения.

Если вы заботитесь о скорости и ориентируетесь на процессор, который поддерживает набор инструкций BMI2 (который будет Intel Haswell и более поздние версии, или AMD Excavator и более поздние PEXT), то вы можете использовать инструкцию PEXT, которая выполняет параллельное извлечение битов. Это позволяет буквально решить всю проблему примерно за две машинные инструкции.

Поскольку вы не пишете в ассемблере, вы должны использовать соответствующий встроенный PEXT инструкции PEXT, а именно _pext_u32. В своей базовой форме код прост, читабелен и чрезвычайно эффективен:

#include <stdint.h>      // for uint32_t
#include <x86intrin.h>   // for _pext_u32()  [on MSVC, drop the 'x86']
void RemoveMatchingBits(uint32_t& a, uint32_t& b)
{
   const uint32_t mask = (a ^ b);
   a = _pext_u32(a, mask);
   b = _pext_u32(b, mask);
}

Во-первых, вы поразрядно-XOR двух значений (a и b вместе). Это создаст маску, где каждый бит в маске устанавливается, если соответствующий бит установлен в a или b, в противном случае этот бит не установлен. Эта маска затем используется в качестве основы для извлечения битов, выполняемого _pext_u32. Одна и та же маска используется для обеих операций извлечения битов, поэтому требуется только одна инструкция XOR. Каждый встроенный _pext_u32 будет компилироваться в инструкцию PEXT. Таким образом, кроме некоторых инструкций MOV для перемешивания значений (которые будут зависеть от компилятора, использованного для генерации кода и от того, встроен ли этот код), требуются только три инструкции машинного кода. Вот как современные версии GCC и Clang компилируют вышеуказанную функцию (MSVC и ICC испускают чрезвычайно похожий код):

RemoveMatchingBits(unsigned int&, unsigned int&):
    mov     eax, DWORD PTR [rdi]    // rdi contains a pointer to 'a'
    mov     edx, DWORD PTR [rsi]    // rsi contains a pointer to 'b'
    xor     edx, eax
    pext    eax, eax, edx
    mov     DWORD PTR [rdi], eax
    mov     eax, DWORD PTR [rsi]
    pext    eax, eax, edx
    mov     DWORD PTR [rsi], eax
    ret

Как вы можете видеть, большинство дополнительных инструкций здесь - это MOV, которые предписаны тем, как мы написали функцию, чтобы принимать ее аргументы путем ссылки и изменять эти значения на месте. Изменение того, как написана функция, и/или получение оптимизатором встроенного кода на сайте вызовов, приведет к еще более эффективной реализации.

Если вы хотите использовать std::bitset, просто немного измените код. Функция- to_ulong() позволяет вам получить доступ к необработанным битам для манипуляции. Что-то вроде:

void RemoveMatchingBits(std::bitset<8>& a, std::bitset<8>& b)
{
   const std::bitset<8> mask = (a ^ b);
   a = _pext_u32(static_cast<uint32_t>(a.to_ulong()), static_cast<uint32_t>(mask.to_ulong()));
   b = _pext_u32(static_cast<uint32_t>(b.to_ulong()), static_cast<uint32_t>(mask.to_ulong()));
}

Обратите внимание, что это еще больше снижает эффективность сгенерированного кода, учитывая необходимость иметь дело с объектом std::bitset. В частности, to_ulong() член to_ulong() должна обнаруживать и to_ulong() исключение в случае переполнения, и MSVC кажется неспособным оптимизировать эту проверку, даже если std::bitset<8> не может переполнить 32-разрядное целое число тип. Ну да ладно - код будет достаточно быстрым, и никто не сказал, что абстракции абсолютно бесплатны.


Если вы не можете скомпилировать, предполагая поддержку BMI2, вы можете проверить во время выполнения, используя инструкцию CPUID (практически все компиляторы x86 предоставляют встроенную функцию для этого).

Если он недоступен, вы не ориентируетесь на x86 или просто не хотите беспокоиться о сложности делегирования во время выполнения, вы можете воспользоваться альтернативной реализацией бит-тиддлинга. В частности, вам нужна операция "сжатия". Обсуждение и код для этого приведены в разделе 7–4 классической книги Генри С. Уоррена-младшего " Хакерское наслаждение".

Вот простая, основанная на циклах реализация "compress", адаптированная из рисунка 7–9 в Hacker Delight:

uint32_t compress(uint32_t value, uint32_t mask)
{
   uint32_t result = 0;
   uint32_t shift  = 0;
   uint32_t maskBit;
   do
   {
        maskBit = (mask & 1);
        result |= ((value & maskBit) << shift);
        shift  += maskBit;
        value >>= 1;
        mask  >>= 1;
    } while (mask != 0);
    return result;
}

Это адекватно имитирует инструкцию PEXT, но это не быстро. Следующий код реализует тот же алгоритм, но использует более быстрый метод "параллельного суффикса", основанный на рисунке 7–10 в Hacker Delight:

uint32_t fallback_pext_u32(uint32_t value, uint32_t mask)
{
   const int log2BitSize = 5;                     // log_2 of the bit size (here, 32 bits)

   value &= mask;                                 // clear irrelevant bits    
   uint32_t mk = (~mask << 1);                    // we will count 0 to the right
   uint32_t mp;
   uint32_t mv;
   uint32_t t;
   for (int i = 0; i < log2BitSize; ++i)
   {
      mp     = mk ^ (mk <<  1);                   // parallel suffix
      mp     = mp ^ (mp <<  2);
      mp     = mp ^ (mp <<  4);
      mp     = mp ^ (mp <<  8);
      mp     = mp ^ (mp << 16);
      mv     = (mp & mask);                       // bits to move
      mask   = ((mask ^ mv) | (mv >> (1 << i)));  // compress mask
      t      = (value & mv);
      value  = ((value ^ t) | (t >> (1 << i)));   // compress value
      mk    &= ~mp;
   }
   return value;
}

Эта резервная реализация будет медленнее, чем одна инструкция PEXT, но она полностью без ветвей, поэтому не будет никаких скрытых штрафов за неправильно предсказанные ветки при работе со случайным вводом. Здесь вы должны получить максимально возможную пропускную способность от вашего ЦП, но в любом случае это, безусловно, будет намного быстрее, чем цикл for с серией условных ветвлений, как предложено в других ответах.

Ответ 2

Вы можете использовать boost::dynamic_bitset<> для результата, затем используя push_back, вы можете динамически создать битсет.

#include <iostream>
#include <boost/dynamic_bitset.hpp>
#include <bitset>

int main()
{
    const int N = 8;
    boost::dynamic_bitset<> a_out(0);
    boost::dynamic_bitset<> b_out(0); 
    std::bitset<N>a(0x97); //10010111
    std::bitset<N>b(0x72); //01110010

    for (int i = 0; i < N; i++)
    {
        if (a[i] != b[i])
        {
            a_out.push_back(bool(a[i]));
            b_out.push_back(bool(b[i]));
        }
    }


    std::cout << a_out << "\n";
    std::cout << b_out << "\n";

    return 0;
}

Попробуйте здесь

Выход:
10011
01100

[Изменено] И если вы хотите оптимизировать, вы можете добавить это перед циклом for (но вы должны увеличить 1.62 или новее использовать reserve())

//@5gon12eder Optimization
const auto xorified = a ^ b;
const auto n = xorified.count();
a_out.reserve(n); 
b_out.reserve(n);

И внутри цикла for сравнивают биты как:

if (xorified[i]) { ... }

Ответ 3

Вам нужно будет написать свой собственный алгоритм. Возможно, что-то подобное:

std::bitset<size> mask = a^b;  //A zero will be put in place where a and b do match
int offset = 0;
std::bitset<size> fin(0);   //This will hold the answer for the a bitset
for (int x = 0; x < size; x++)
{
  if (!mask[x])  //If the bit is zero we are keeping the bit
  {
    if (a[x])
    {
      fin.set(offset);
    }
    offset++;
  }
}

Ответ 4

Все вычисленные во время компиляции

Демо (требуется С++ 17)

Другие ответы здесь превосходны и что вы должны предпочесть в общем случае, потому что, вероятно, вы не будете знать, что представляют собой первые два бита.

Однако это не весело. Для вашего конкретного примера у нас есть достаточно информации, чтобы решить все это во время компиляции и с помощью constexpr if, variadic templates, шаблон переменной, а целые последовательности * мы можем выполнить все вычисления и преобразование в строковый литерал (для инициализации битового набора) во время компиляции.

Подход

  • Представляем биты как целые последовательности
    • std::integer_sequence<int,1,0,0,1,0,1,1,1> и std::integer_sequence<int,0,1,1,1,0,0,1,0>
  • Отфильтруйте последовательности в соответствии с вашей логикой (одни и те же биты в том же положении удалены)
  • Преобразование целочисленных последовательностей в последовательности char
    • Я имею в виду std::integer_sequence<char, ...>
  • Используйте шаблон переменной для преобразования последовательности char в строковый литерал с нулевым завершением, который можно использовать для построения std::bitset
    • Размер создаваемого битового набора можно получить из результирующего std::integer_sequence<int, ...> с помощью функции size():

Полный код:

#include <iostream>
#include <utility>
#include <bitset>

// sequence concatenation
template <typename INT, INT ...s, INT ...t>
constexpr auto
concat_sequence(std::integer_sequence<INT,s...>,std::integer_sequence<INT,t...>){
   return std::integer_sequence<INT,s...,t...>{};
}

// base case; empty sequence
template<class INT, INT a, INT b>
constexpr auto Filter(std::integer_sequence<INT, a>, std::integer_sequence<INT, b>)
{
    if constexpr (a == b)
        return std::integer_sequence<INT>{};
    else
        return std::integer_sequence<INT,a>{};
}

template<class INT>
constexpr auto Filter(std::integer_sequence<INT>, std::integer_sequence<INT>)
{
   return std::integer_sequence<INT>{};
}

// recursive case
template<class INT, INT a, INT... b, INT c, INT... d>
constexpr auto Filter(std::integer_sequence<INT, a, b...>, std::integer_sequence<INT, c, d...> )
{
    static_assert(sizeof...(b) == sizeof...(d), "Sequences should initially be the same length");
    return concat_sequence(Filter(std::integer_sequence<INT, a>{}, std::integer_sequence<INT, c>{}),
                           Filter(std::integer_sequence<INT, b...>{}, std::integer_sequence<INT, d...>{}));
}

// for constructing bitset/printing
template <char... s>
using char_sequence=std::integer_sequence<char,s...>;

template <char ...s>
constexpr static char const make_char_string[]={s... , '\0'};

template <char ...s>
constexpr auto const & make_char_string_from_sequence(char_sequence<s...>){
   return make_char_string<s...>;
}

template<class INT, INT digit>
constexpr auto make_binary_charseq()
{
    static_assert(digit < 2, "binary digits are 0 and 1 only");
    return char_sequence<digit == 1? '1' : '0'>{};
}

template <class INT, INT... elts>
struct convert_binary_to_charseq_impl;

template <class INT, INT n, INT ...rest>
constexpr auto convert_binary_to_charseq(std::integer_sequence<INT, n, rest...>){
   return concat_sequence(make_binary_charseq<INT, n>(),
                          convert_binary_to_charseq_impl<INT, rest...>{}());
}

template <class INT, INT... elts>
struct convert_binary_to_charseq_impl{
   constexpr auto operator()()const {
      return convert_binary_to_charseq<INT, elts...>(std::integer_sequence<INT, elts...>{});
   }
};

template <class INT>
struct convert_binary_to_charseq_impl<INT>{
   constexpr auto operator()()const{
      return char_sequence<>{};
   }
};

и наш тест:

int main()
{
    using left_result = decltype(Filter(std::integer_sequence<int,1,0,0,1,0,1,1,1>{}, std::integer_sequence<int,0,1,1,1,0,0,1,0>{}));
    using right_result = decltype(Filter(std::integer_sequence<int,0,1,1,1,0,0,1,0>{}, std::integer_sequence<int,1,0,0,1,0,1,1,1>{}));

    static_assert(std::is_same_v<left_result, std::integer_sequence<int, 1,0,0,1,1>>, "Filtering did not work");
    static_assert(std::is_same_v<right_result, std::integer_sequence<int, 0,1,1,0,0>>, "Filtering did not work");

    std::bitset<left_result::size()> a(make_char_string_from_sequence(convert_binary_to_charseq(left_result{})));
    std::bitset<right_result::size()> b(make_char_string_from_sequence(convert_binary_to_charseq(right_result{})));

    std::cout << a << std::endl;
    std::cout << b << std::endl;
}

Вывод:

10011
01100

Недостатком здесь является то, что я эффективно делаю расчет дважды, но я уверен, что он может быть переработан (и все это во время компиляции, поэтому нам все равно, правильно!?)

* Кредит, на который рассчитывается кредит: беседа с Питером Соммерладом CppCon2015 была бесценна для преобразования последовательности в строку. Слайды

Ответ 5

Если вы используете std:: bitset, вы можете сначала использовать оператор XOR. Это даст вам новый битсет, заполненный 0 на индексах, где значения одинаковы, и 1 в противном случае. После этого вы просто удаляете индексы, в которых новый битсет имеет 0.

Ответ 6

Вы не можете удалить биты из std::bitset, чтобы ваш результат имел дополнительные нули. Я имею в виду результат вместо 10011 будет 00010011

constexpr int num = 8;
std::bitset<num> a("10010111");
std::bitset<num> b("01110010");
std::bitset<num> a_result;
std::bitset<num> b_result;

unsigned int last_index = 0;
for(auto index = 0; index < num; ++index)
{
    if(a.test(index) ^ b.test(index))
    {
        a_result.set(last_index, a.test(index));
        b_result.set(last_index, b.test(index));

        ++last_index;
    }
}

Или вы можете использовать std::vector<bool> в качестве результата, то есть специализацию std::vector для bool, которая использует внутренние биты (на самом деле это реализация определена). Все возможные решения зависят от того, чего вы хотите достичь.

constexpr int num = 8;
std::bitset<num> a("10010111");
std::bitset<num> b("01110010");
std::vector<bool> a_result;
std::vector<bool> b_result;

for(auto index = 0; index < num; ++index)
{
    if(a.test(index) ^ b.test(index))
    {
        a_result.push_back(a.test(index));
        b_result.push_back(b.test(index));
    }
}

Ответ 7

Вы пытаетесь использовать этот алгоритм

void Procedure(void)
{
unsigned char NumA, NumB;
unsigned char ResA = 0, ResB = 0;
int Count1 = 0;
int Count2 = 8;

NumA = 0x97; // 10010111
NumB = 0x72; // 01110010
while( Count1 < 8 )
    {
    if( (NumA & 0x80) != (NumB & 0x80) )
        {
        ResA = ResA << 1;
        if( (NumA & 0x80) == 0x80)
            ResA = ResA | 0x01;
        ResB = ResB << 1;
        if( (NumB & 0x80) == 0x80)
            ResB = ResB | 0x01;
        --Count2;
        }
    NumA = NumA << 1;
    NumB = NumB << 1;
    ++Count1;
    }
ResA = ResA << Count2;
ResB = ResB << Count2;
}

Результат сохраняется в переменных ResA и ResB

Ответ 8

Вы не можете получить результат типа bitset, потому что вам нужно установить размер битов во время компиляции, когда вы на самом деле не знаете, сколько позиций бит равно.