比较位集的最快方法(<位集上的运算符)?

Fastest way to compare bitsets (< operator on bitsets)?

本文关键字:运算符 lt 方法 比较      更新时间:2023-10-16

对于对应于无符号整数表示比较的std::bitset实现<运算符的最优化方法是什么(它应该适用于more than 64 bits的位集)?

一个简单的实现是:

template<std::size_t N>
bool operator<(const std::bitset<N>& x, const std::bitset<N>& y)
{
for (int i = N-1; i >= 0; i--) {
if (x[i] && !y[i]) return false;
if (!x[i] && y[i]) return true;
}
return false;
}

当我说"最优化的方式"时,我正在寻找使用按位运算和元编程技巧(以及类似的东西)的实现。

编辑:我认为我已经找到了诀窍:用于编译时递归和右位移的模板元编程,以便将位集作为几个无符号的长长整数进行比较。但是不清楚该怎么做...

明显的优化是

template<std::size_t N>
bool operator<(const std::bitset<N>& x, const std::bitset<N>& y)
{
for (int i = N-1; i >= 0; i--) {
if (x[i] ^ y[i]) return y[i];
}
return false;
}

除此之外,每次测试使用更多的位应该是完全不可能的,因为没有符合标准的方式来访问它们。您可以对x.to_string() < y.to_string()进行基准测试,并希望to_string()和字符串比较都比按位访问bitset更好地优化,但这是一个很长的机会。

如果您愿意在 STL 位集更改时采用该解决方案,您可以使用

template<int n>
bool compare(bitset<n>& l, bitset<n>& r){
if(n > 64){
typedef array<long, (n/64)> AsArray;
return *reinterpret_cast<AsArray*>(&l)
< *reinterpret_cast<AsArray*>(&r);
}//else
return l.to_ulong() < r.to_ulong();
}

编译器丢弃 if 的不相关分支

虽然你说位集,但你真的不是在谈论任意精度无符号整数比较吗? 如果是这样,那么你可能不会轻易地做得比包装GMP更好。

从他们的网站:

GMP经过精心设计,尽可能快,无论是针对小型 操作数和巨大的操作数。速度是通过使用 全词作为基本算术类型,通过使用快速算法,与 高度优化的汇编代码,适用于 很多CPU,并且通常强调速度。

考虑它们的整数函数

我只是看了源代码,但不幸的是(除非,希望我弄错了),它们似乎没有让您就地访问特定位块的const & unsigned long。如果他们这样做了,那么您可以执行模板递归,并有效地比较每个unsigned long,而不是无符号长整型中的每个位。

毕竟,如果A < B,那么不仅每个最重要的位应该a <= b,每个最重要的块也应该A[i] <= B[i]

我不想这么说,但我可能会在 C++11 的std::array上使用递归来滚动我自己的。如果你可以访问这些块,那么你可以很容易地创建一个模板递归函数来做到这一点(我相信你知道,因为你要求元编程)给编译器一个很好的优化机会。

总而言之,这不是一个很好的答案,但这就是我会做的。

顺便说一下,很好的问题。

=

==========

编辑

这应该有三种方法:具有最新点赞的方法,我描述的区块策略和模板递归变体。我用位集填充向量,然后使用指定的比较器函子重复排序。

快乐黑客!

我的计算机上的输出:

运行时间: 编译的 g++ -std=c++11 -Wall -g test.cpp std::bitset 4530000(OP 中的 6000000 原文) 逐块 900000 模板递归 730000 编译的 g++ -std=c++11 -Wall -g -O3 test.cpp 运行时间: std::bitset 700000(OP 中的 740000 原文) 逐块 470000 模板递归 530000

C++11 代码:

#include <iostream>
#include <bitset>
#include <algorithm>
#include <time.h>
/* Existing answer. Note that I've flipped the order of bit significance to match my own */
template<std::size_t N>
class BitByBitComparator
{
public:
bool operator()(const std::bitset<N>& x, const std::bitset<N>& y) const
{
for (int i = 0; i < N; ++i) {
if (x[i] ^ y[i]) return y[i];
}
return false;
}
};
/* New simple bit set class (note: mostly untested). Also note bad
design: should only allow read access via immutable facade. */
template<std::size_t N>
class SimpleBitSet
{
public:
static const int BLOCK_SIZE = 64;
static const int LOG_BLOCK_SIZE = 6;
static constexpr int NUM_BLOCKS = N >> LOG_BLOCK_SIZE;
std::array<unsigned long int, NUM_BLOCKS> allBlocks;
SimpleBitSet()
{
allBlocks.fill(0);
}
void addItem(int itemIndex)
{
// TODO: can do faster
int blockIndex = itemIndex >> LOG_BLOCK_SIZE;
unsigned long int & block = allBlocks[blockIndex];
int indexWithinBlock = itemIndex % BLOCK_SIZE;
block |= (0x8000000000000000 >> indexWithinBlock);
}
bool getItem(int itemIndex) const
{
int blockIndex = itemIndex >> LOG_BLOCK_SIZE;
unsigned long int block = allBlocks[blockIndex];
int indexWithinBlock = itemIndex % BLOCK_SIZE;
return bool((block << indexWithinBlock) & 0x8000000000000000);
}
};
/* New comparator type 1: block-by-block. */
template<std::size_t N>
class BlockByBlockComparator
{
public:
bool operator()(const SimpleBitSet<N>& x, const SimpleBitSet<N>& y) const
{
return ArrayCompare(x.allBlocks, y.allBlocks);
}
template <std::size_t S>
bool ArrayCompare(const std::array<unsigned long int, S> & lhs, const std::array<unsigned long int, S> & rhs) const
{
for (int i=0; i<S; ++i)
{
unsigned long int lhsBlock = lhs[i];
unsigned long int rhsBlock = rhs[i];
if (lhsBlock < rhsBlock) return true;
if (lhsBlock > rhsBlock) return false;
}
return false;
}
};
/* New comparator type 2: template recursive block-by-block. */
template <std::size_t I, std::size_t S>
class TemplateRecursiveArrayCompare;
template <std::size_t S>
class TemplateRecursiveArrayCompare<S, S>
{
public:
bool operator()(const std::array<unsigned long int, S> & lhs, const std::array<unsigned long int, S> & rhs) const
{
return false;
}
};
template <std::size_t I, std::size_t S>
class TemplateRecursiveArrayCompare
{
public:
bool operator()(const std::array<unsigned long int, S> & lhs, const std::array<unsigned long int, S> & rhs) const
{
unsigned long int lhsBlock = lhs[I];
unsigned long int rhsBlock = rhs[I];
if (lhsBlock < rhsBlock) return true;
if (lhsBlock > rhsBlock) return false;
return TemplateRecursiveArrayCompare<I+1, S>()(lhs, rhs);
}
};
template<std::size_t N>
class TemplateRecursiveBlockByBlockComparator
{
public:
bool operator()(const SimpleBitSet<N>& x, const SimpleBitSet<N>& y) const
{
return TemplateRecursiveArrayCompare<x.NUM_BLOCKS, x.NUM_BLOCKS>()(x.allBlocks, y.allBlocks);
}
};
/* Construction, timing, and verification code */
int main()
{
srand(0);
const int BITSET_SIZE = 4096;
std::cout << "Constructing..." << std::endl;
// Fill a vector with random bitsets
const int NUMBER_TO_PROCESS = 10000;
const int SAMPLES_TO_FILL = BITSET_SIZE;
std::vector<std::bitset<BITSET_SIZE> > allBitSets(NUMBER_TO_PROCESS);
std::vector<SimpleBitSet<BITSET_SIZE> > allSimpleBitSets(NUMBER_TO_PROCESS);
for (int k=0; k<NUMBER_TO_PROCESS; ++k)
{
std::bitset<BITSET_SIZE> bs;
SimpleBitSet<BITSET_SIZE> homemadeBs;
for (int j=0; j<SAMPLES_TO_FILL; ++j)
{
int indexToAdd = rand()%BITSET_SIZE;
bs[indexToAdd] = true;
homemadeBs.addItem(indexToAdd);
}
allBitSets[k] = bs;
allSimpleBitSets[k] = homemadeBs;
}
clock_t t1,t2,t3,t4;
t1=clock();
std::cout << "Sorting using bit-by-bit compare and std::bitset..."  << std::endl;
const int NUMBER_REPS = 100;
for (int rep = 0; rep<NUMBER_REPS; ++rep)
{
auto tempCopy = allBitSets;
std::sort(tempCopy.begin(), tempCopy.end(), BitByBitComparator<BITSET_SIZE>());
}
t2=clock();
std::cout << "Sorting block-by-block using SimpleBitSet..."  << std::endl;
for (int rep = 0; rep<NUMBER_REPS; ++rep)
{
auto tempCopy = allSimpleBitSets;
std::sort(tempCopy.begin(), tempCopy.end(), BlockByBlockComparator<BITSET_SIZE>());
}
t3=clock();
std::cout << "Sorting block-by-block w/ template recursion using SimpleBitSet..."  << std::endl;
for (int rep = 0; rep<NUMBER_REPS; ++rep)
{
auto tempCopy = allSimpleBitSets;
std::sort(tempCopy.begin(), tempCopy.end(), TemplateRecursiveBlockByBlockComparator<BITSET_SIZE>());
}
t4=clock();
std::cout << std::endl << "RUNTIMES:" << std::endl;
std::cout << "tstd::bitset        t" << t2-t1 << std::endl;
std::cout << "tBlock-by-block     t" << t3-t2 << std::endl;
std::cout << "tTemplate recursive t" << t4-t3 << std::endl;
std::cout << std::endl;
std::cout << "Checking result... ";
std::sort(allBitSets.begin(), allBitSets.end(), BitByBitComparator<BITSET_SIZE>());
auto copy = allSimpleBitSets;
std::sort(allSimpleBitSets.begin(), allSimpleBitSets.end(), BlockByBlockComparator<BITSET_SIZE>());
std::sort(copy.begin(), copy.end(), TemplateRecursiveBlockByBlockComparator<BITSET_SIZE>());
for (int k=0; k<NUMBER_TO_PROCESS; ++k)
{
auto stdBitSet = allBitSets[k];
auto blockBitSet = allSimpleBitSets[k];
auto tempRecBlockBitSet = allSimpleBitSets[k];
for (int j=0; j<BITSET_SIZE; ++j)
if (stdBitSet[j] != blockBitSet.getItem(j) || blockBitSet.getItem(j) != tempRecBlockBitSet.getItem(j))
std::cerr << "error: sorted order does not match" << std::endl;
}
std::cout << "success" << std::endl;
return 0;
}

检查异或的最高位怎么样?

bool operator<(const std::bitset<N>& x, const std::bitset<N>& y)
{
return y[fls(x^y)]
}
int fls(const std::bitset<N>& n) {
// find the last set bit
}

http://uwfsucks.blogspot.be/2007/07/fls-implementation.html,可以在这里找到一些fps的想法。

嗯,有很好的老memcmp.从某种意义上说,它是脆弱的,因为它取决于std::bitset的实施。因此可能无法使用。但可以合理地假设模板会创建一个不透明的int数组。并且没有其他簿记领域。

template<std::size_t N>
bool operator<(const std::bitset<N>& x, const std::bitset<N>& y)
{
int cmp = std::memcmp(&x, &y, sizeof(x));
return (cmp < 0);
}

这将唯一确定bitset的顺序。但这可能不是人类的直觉秩序。这取决于哪些位用于哪个集合成员索引。例如,索引 0 可以是第一个 32 位整数的 LSB。或者它可能是前 8 位字节的 LSB。

强烈建议使用单元测试,以确保它实际上适用于它的使用>方式。

只有在两个位集不同时才执行按位比较,已经产生了一些性能提升:

template<std::size_t N>
bool operator<(const std::bitset<N>& x, const std::bitset<N>& y)
{       if (x == y)
return false;
….
}

我知道这是一个有点老的问题,但是如果您知道位集的最大大小,则可以像这样创建sth:

class Bitset{
vector<bitset<64>> bits;
/*
* operators that you need
*/
};

这允许您将每个bitsets<64>转换为unsigned long long以便快速比较。如果你想到达特定的位(为了改变它或其他什么),你可以做bits[id / 64][id % 64]

bitset 的底层实现在几乎所有 64 位 CPU、编译器等中使用 uint64,因为只有一种明智的方法可以使用给定接口编写类的实现,这使得很容易找出"可移植"的黑客。

因此,假设您只想要"明显"有效的方法来做到这一点,并且您的代码不会用于控制核武库,那么完全知道这将使您的保修失效,yadda yadda yadda,这是您正在寻找的代码:

template <int N> bool operator<(const bitset<N> & a, const bitset<N> & b) {
const uint64_t * p = (const uint64_t *)(&a);
const uint64_t * q = (const uint64_t *)(&b);
const uint64_t * r = p;
int i= (sizeof(bitset<N>)-1)/sizeof(uint64_t);
for (p+=i, q+=i; (p>=r) && (*p==*q); --p, --q) {}
return *p<*q;
}

基本上转换为 uint64 数组并以相反的顺序逐个元素比较,直到发现差异。

还要注意,这假设 x86-64 字节序。