检测C++中的匹配位

Detecting matching bits in C++

本文关键字:C++ 检测      更新时间:2023-10-16

>例如,我正在尝试获取两个bitset对象

a = 10010111
b = 01110010

并从两个变量中删除位,如果它们在同一位置/索引中匹配。所以我们只剩下

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

有什么办法可以做到这一点吗?

其他答案显示了很好的、惯用的C++方法。不幸的是,它们会相当慢。即使是 AndyG 聪明的基于模板的解决方案,尽管它在编译时确实做了尽可能多的工作,但仍然会导致编译器生成大量必须在运行时执行的代码。

如果您关心速度并且针对支持 BMI2 指令集的处理器(可能是英特尔 Haswell 及更高版本,或 AMD Excavator 及更高版本),那么您可以使用执行并行位提取的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);
}

首先,按位或两个值(ab一起)。这将生成一个掩码,如果相应的位设置为ab,则设置掩码中的每个位,否则不设置该位。然后,该掩码用作_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()成员函数必须在溢出的情况下检测并引发异常,并且 MSVC 似乎无法优化该签出,即使std::bitset<8>不可能溢出 32 位整数类型也是如此。哦,好吧——代码会足够快,没有人说抽象是完全免费的。


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

如果它不可用,则您不以 x86 为目标,或者如果您只是不想担心运行时委派的复杂性,则可以回退到替代的位抖动实现。具体来说,您想要的是"压缩"操作。对此的讨论和代码在小亨利·S·沃伦(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指令,但速度并不快。以下代码实现了相同的算法,但使用基于黑客喜悦中的图 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)
value &= mask;                                 // clear irrelevant bits    
uint32_t mk = (~mask << 1);                    // we will count 0's 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指令慢,但它是完全无分支的,因此在处理随机输入时不会对错误预测的分支进行任何隐藏的惩罚。您应该在这里从 CPU 获得最大可能的吞吐量,但无论哪种方式,它肯定比具有一系列条件分支的for循环快得多,正如其他答案所建议的那样。

您可以使用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循环之前添加它(但你必须有 boost 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]) { ... }

编译时计算的所有内容

演示(需要 C++17)

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

但是,这并不好玩。对于您的特定示例,我们确实有足够的信息在编译时解决所有问题,并且通过使用constexpr if,可变参数模板,变量模板和整数序列*,我们可以在编译时执行所有计算和转换为字符串文字(用于初始化位集)。

A. 方法

  • 将位集表示为整数序列
    • 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转换为字符序列
    • 我的意思是std::integer_sequence<char, ...>
  • 使用变量模板将 char 序列转换为可用于构造std::bitset的以 null 结尾的字符串文本
    • 要创建的位集的大小可以通过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... , ''};
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 演讲对于将序列转换为字符串非常宝贵。幻灯片

您将需要编写自己的算法。 像这样的东西可能会起作用:

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

如果您使用的是 std::bitset,则可以先使用 XOR 运算符。 这将为您提供新的位集,在值相同的索引上填充 0,否则填充 1。 之后,您只需删除新位集具有 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>作为结果,这是内部使用位集的bool的专用std::vector(实际上是定义的实现)。所有可能的解决方案都取决于您要实现的目标。

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

您尝试使用此算法

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

这是我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);
}

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