检测C ++中的匹配位

时间:2017-01-18 13:06:39

标签: c++

我试图接受两个bitset个对象,例如

a = 10010111
b = 01110010

并删除两个变量中的位,如果它们在相同的位置/索引中匹配。所以我们留下了

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

有没有办法实现这个目标?

9 个答案:

答案 0 :(得分:3)

其他答案显示了很好的,惯用的C ++方法。不幸的是,它们会变得相当缓慢。即使AndyG's clever template-based solution,尽管它在编译时尽可能多地完成了工作,但仍会导致编译器生成大量必须在运行时执行的代码。

如果您关心速度并且定位的是支持BMI2 instruction set的处理器(可能是Intel Haswell及更高版本,或AMD挖掘机及更高版本),那么您可以使用执行的PEXT instruction a parallel bit extraction。这使您可以在大约两个机器指令中解决整个问题。

由于您没有在汇编语言中编写,因此您将使用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);
}

首先,您将两个值(ab一起按位XOR)。这将生成一个掩码,如果相应的位在 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 s,由我们编写函数以通过引用接受其参数并在适当位置修改这些值的方式强制执行。调整函数的编写方式,和/或让优化器在调用站点内联函数,将会产生更高效的实现。

如果您想使用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()成员函数必须在溢出的情况下检测并抛出异常,并且MSVC似乎无法优化该检出,即使std::bitset<8>不可能溢出32位整数类型。好吧 - 代码将足够快,没有人说抽象是完全免费的。

如果您无法编译假设BMI2支持,您可以在运行时使用CPUID instruction进行检查(几乎所有x86编译器都为此提供内在函数。)

如果它不可用,那么您不是以x86为目标,或者如果您只是不想担心运行时委派的复杂性,那么您可以回到另一个有点笨拙的实现。具体来说,你想要的是“压缩”操作。关于这方面的讨论和代码在Henry S. Warren,Jr。的经典着作Hacker's Delight的第7-4节中给出。

这是一个简单的,基于循环的“压缩”实现,改编自 Hacker's Delight 中的图7-9:

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指令,但速度并不快。以下代码实现了相同的算法,但在 Hacker's Delight 中使用了基于图7-10的更快的“并行后缀”方法:

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

   uint32_t mk = (~mask << 1);                    // we will count 0's to the right
   uint32_t mp;
   uint32_t mv;
   uint32_t t;
   value = (value & mask);                        // clear irrelevant bits

   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指令慢,但它完全没有分支,因此在处理随机输入时,错误预测的分支不会有任何隐藏的惩罚。您应该从CPU获得最大可能的吞吐量,但无论哪种方式,它肯定比具有一系列条件分支的for循环快得多,如其他答案所建议的那样。

答案 1 :(得分: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;
}

Try here!

  

输出:
10011
01100

[EDITED] 如果你想进行优化,你可以在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]) { ... }

答案 2 :(得分:1)

您将需要编写自己的算法。这样的事情可能有用:

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++;
  }
}

答案 3 :(得分:1)

编译时计算的所有内容

Demo(需要C ++ 17)

这里的其他答案都很棒,在一般情况下你应该更喜欢什么,因为你很可能不知道最初的两个比特集是什么。

然而,这没什么好玩的。对于您的具体示例,我们有足够的信息在编译时解决所有问题,并使用constexpr ifvariadic templatesvariable template,和integer sequences * 我们可以在编译时执行所有计算和转换为字符串文字(用于初始化bitset)。

方法

  • 将位集表示为整数序列
    • std::integer_sequence<int,1,0,0,1,0,1,1,1>std::integer_sequence<int,0,1,1,1,0,0,1,0>
  • 根据您的逻辑过滤序列(删除相同位置的相同位)
  • 将integer_sequences转换为char序列
    • 我的意思是std::integer_sequence<char, ...>
  • 使用变量模板将char序列转换为以null结尾的字符串文字,可用于构造std::bitset
    • 可以通过size()成员函数从生成的std::integer_sequence<int, ...>获取要创建的位集的大小:

完整代码:

#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

这方面的缺点是我有效地进行了两次计算,但我确信它可以重做(这都是在编译时所以我们不在乎,对吧!?)

* 信用到期的信用:Peter Sommerlad的CppCon2015谈话对于将序列转换为字符串非常宝贵。 Slides

答案 4 :(得分:0)

如果你正在使用std :: bitset,你可以先使用XOR运算符。 这将为您提供新的bitset,在值相同的索引上填充0,否则为1。 之后,您只需删除新bitset为0的索引。

答案 5 :(得分:0)

您无法从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));
    }
}

答案 6 :(得分:0)

您尝试使用此算法

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变量

答案 7 :(得分:0)

这是我的 C++ 解决方案:

#include <iostream>
#include <bits/stdc++.h>

pair<int, int> extractMatchingBits(int a, int b) {
  int cleanA = 0;
  int cleanB = 0;
  int matches = a^b;
  for (int i = 0; matches != 0; i++) {
    const int bitIdx = log2(matches & -matches);
    
    cleanA |= ((a >> bitIdx) & 1) << i;
    cleanB |= ((b >> bitIdx) & 1) << i;
    
    matches &= matches - 1;
  }
  
  return make_pair(cleanA, cleanB);
}

答案 8 :(得分:-1)

您不能拥有bitset类型的结果,因为您必须在编译时设置位集大小,而实际上您并不知道位数相等的位数。