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

Posted

技术标签:

【中文标题】比较位集的最快方法(位集上的 < 运算符)?【英文标题】:Fastest way to compare bitsets (< operator on bitsets)? 【发布时间】:2014-02-10 06:43:37 【问题描述】:

std::bitset 实现&lt; 运算符的最优化方法是什么,对应于无符号整数表示的比较(它应该适用于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;

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

编辑:我认为我找到了诀窍:用于编译时递归和右位移位的模板元编程,以便将位集作为几个无符号长整型进行比较。但是没有明确的想法如何做到这一点......

【问题讨论】:

关于使用右位移位的想法:这会创建很多中间对象,to_ullong 必须检查移位后的值是否适合unsigned long long对于每次检查,因此会产生相当多的开销。我怀疑它会更快,尽管只有基准可以证明这一点。 复制 std::bitset 的代码,重命名,给它一个方法来一次访问一个单词。 @brianbeuning 如果您仍然要复制代码,您只需提供一个operator&lt; 即可访问内部。 【参考方案1】:

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

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

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

【讨论】:

问题:优化fls 需要与原始问题一样多的内部访问bitset。【参考方案2】:

明显的优化是

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() &lt; y.to_string() 进行基准测试,并希望to_string() 和字符串比较能够比按位访问bitset 得到更好的优化,但这是一个长期的目标。

【讨论】:

@dyp 谁知道呢。这是一个性能问题,所以最后你必须对其进行基准测试。它可能会随着每个编译器版本而改变。如果考虑“小”位集,也可以使用 to_ullong 专门针对 +1 对于它的大小的解决方案,很难做得更好。模板递归版本见下文。 请注意,即使std::bitset&lt;N&gt; 会公开一些.data() 成员,标准容器和std::tuple 的字典顺序也很难使用这些知识进行优化。诱人的做法是对底层单词表示进行整数比较,但这实际上对应于 reverse colexicographical 排序。您可以将std::lexicographical_compare(rbegin(R.data), rend(R.data), rbegin(L.data), rend(L.data)) 用作operator&lt;(L, R)。 “reverse”对应于 L/R 反转,“co”对应于“reverse colexicographical”中的反向迭代器。【参考方案3】:

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

毕竟,如果A &lt; B,那么不仅每个最高有效位a &lt;= b,每个最高有效块A[i] &lt;= 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;

【讨论】:

-O3和最近的gcc编译,第二个选项最快,第三个非常接近,第一个是第二个的1.5倍。【参考方案4】:

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

来自他们的网站:

GMP 经过精心设计,以尽可能快的速度运行,无论是小型 操作数和巨大的操作数。速度是通过使用来实现的 全字作为基本算术类型,通过使用快速算法, 高度优化的汇编代码,用于最常见的内部循环 很多 CPU,并且普遍强调速度。

考虑their integer functions

【讨论】:

【参考方案5】:

如果您愿意在 STL bitset 发生变化时采用该解决方案,您可以使用

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 away 的无关分支

【讨论】:

【参考方案6】:

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

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

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

强烈推荐单元测试,以确保它确实适用于它的使用方式。 ;->

【讨论】:

【参考方案7】:

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

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

【讨论】:

如果它们总是不同的话就不会。【参考方案8】:

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

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

这允许您将每个bitsets&lt;64&gt; 转换为unsigned long long 以进行快速比较。如果你想获得特定的位(为了改变它或其他什么)你可以做bits[id / 64][id % 64]

【讨论】:

以上是关于比较位集的最快方法(位集上的 < 运算符)?的主要内容,如果未能解决你的问题,请参考以下文章

相当于 Visual Studio 中位集的 .Find_first 方法

在初始化时定义位集大小?

生成大小为 1 到 n 的所有组合(位集)

Cuda:XOR 单个位集与位集数组

移动 Java 位集

从整数数组构造位集