【问题标题】:boost::dynamic_bitset slower than std::bitset unless std::bitset is resetboost::dynamic_bitset 比 std::bitset 慢,除非 std::bitset 被重置
【发布时间】:2014-10-13 02:40:55
【问题描述】:

我最近遇到了 bitset 模板,我真的很想在我当前的项目中使用它们。继续阅读,我看到std::bitset 模板的大小必须在编译时确定。许多人建议使用boost::dynamic_bitset 来缓解这一要求。

为了比较两者,我决定对set、flip 和count 方法进行速度比较。

结果很奇怪......我想知道是否有人可以为我解释一下。

代码在帖子的末尾,但我会在这里解释我在做什么。我有一个std::bitset 对象(称为bs)和一个boost::dynamic_bitset 对象(称为dynbs)。每个都有n=1000000 位。对于上面的给定方法,依次调用每个n 位上的方法并重复此R=10000 次。

使用std::chrono 库,以下是每个以纳秒为单位的计时:

set
        bitset:              267 nsecs
    dyn bitset:      18603174546 nsecs

flip
        bitset:               73 nsecs
    dyn bitset:      18842352867 nsecs

count
        bitset:               77 nsecs
    dyn bitset:               51 nsecs

对于set 和flip,boost::dynamic_bitset 似乎要慢得多。

为了更有趣,如果在运行这些测试之前对两个对象调用方法reset,那么时间是可比较的。他们在这里:

set
        bitset:      19397779399 nsecs
    dyn bitset:      18472863864 nsecs

flip
        bitset:      18599248629 nsecs
    dyn bitset:      18376267939 nsecs

count
        bitset:               68 nsecs
    dyn bitset:               61 nsecs

现在,两个容器都声称将所有位初始化为0,因此对reset 的调用不应更改任何位。在reset 之前和之后转储none 的输出确实证实了这一点。

毕竟,我有两个问题:

1) 为什么在调用set 和flip 时boost::dynamic_bitset 比std::bitset 慢很多?

2) 为什么调用reset会对std::bitset的速度产生巨大的负面影响?

这是我的代码:

#include <iostream>
#include <iomanip>
#include <bitset>
#include <boost/dynamic_bitset.hpp>
#include <vector>
#include <chrono>
#include <ctime>

using namespace std;
using namespace chrono;
using namespace boost;

int main(){
  const unsigned int n=1000000;
  bitset< n > bs;
  dynamic_bitset< > dynbs(n);
  // bs.reset();
  // dynbs.reset();

  unsigned int i,r,R=10000;
  high_resolution_clock::time_point tick,tock;


  ////////////////////////////////////////////////////////////
  // Method: set

  std::cout << "set" << std::endl;

  tick=high_resolution_clock::now();

  for(r=0; r<R; r++)
    for(i=0; i<n; i++)
      bs.set(i);

  tock=high_resolution_clock::now();
  cout << setw(16) << "bitset: " 
       << setw(16) << duration_cast<nanoseconds>(tock-tick).count() << " nsecs" 
       << std::endl;

  tick=high_resolution_clock::now();

  for(r=0; r<R; r++)
    for(i=0; i<n; i++)
      dynbs.set(i);

  tock=high_resolution_clock::now();
  cout << setw(16) << "dyn bitset: " 
       << setw(16) << duration_cast<nanoseconds>(tock-tick).count() << " nsecs"
       << std::endl << std::endl;


  ////////////////////////////////////////////////////////////
  // Method: flip

  std::cout << "flip" << std::endl;

  tick=high_resolution_clock::now();

  for(r=0; r<R; r++)
    for(i=0; i<n; i++)
      bs.flip(i);

  tock=high_resolution_clock::now();
  cout << setw(16) << "bitset: " 
       << setw(16) << duration_cast<nanoseconds>(tock-tick).count() << " nsecs"
       << std::endl;

  tick=high_resolution_clock::now();

  for(r=0; r<R; r++)
    for(i=0; i<n; i++)
      dynbs.flip(i);

  tock=high_resolution_clock::now();
  cout << setw(16) << "dyn bitset: " 
       << setw(16) << duration_cast<nanoseconds>(tock-tick).count() << " nsecs"
       << std::endl << std::endl;


  ////////////////////////////////////////////////////////////
  // Method: count

  std::cout << "count" << std::endl;

  tick=high_resolution_clock::now();

  for(r=0; r<R; r++)
    for(i=0; i<n; i++)
      bs.count();

  tock=high_resolution_clock::now();
  cout << setw(16) << "bitset: " 
       << setw(16) << duration_cast<nanoseconds>(tock-tick).count() << " nsecs"
       << std::endl;

  tick=high_resolution_clock::now();

  for(r=0; r<R; r++)
    for(i=0; i<n; i++)
      dynbs.count();

  tock=high_resolution_clock::now();
  cout << setw(16) << "dyn bitset: " 
       << setw(16) << duration_cast<nanoseconds>(tock-tick).count() << " nsecs"
       << std::endl;


  return 0;
}

我用它编译过

g++ -O3 -std=c++11 bitset.cpp -o bitset

bitset.cpp 是上面插入的代码。

【问题讨论】:

  • @T.C.只是我的测试太简单了吗?我想我不太确定这意味着什么。我试图阅读汇编代码,但不太明白发生了什么。
  • 差不多。微基准很难编写。在这种情况下,如果在每个循环之后添加要打印的代码,例如 bs,那么 sets 和 flips 可能不会被优化掉。为确保count 调用不会被优化,请将返回值添加到一个变量中,然后打印出来。 dynamic_bitset 使用堆上的分配,编译器很难证明set 和flip 所做的更改将是不可见的。
  • @T.C.明白了。这就说得通了。感谢您澄清幕后发生的事情。刚刚添加了你提到的一些东西,现在看起来是一场公平的战斗!我将发布一个引用您所说的解决方案并将其标记为已解决。

标签: c++ boost-dynamic-bitset std-bitset


【解决方案1】:

1) 为什么boost::dynamic_bitset 比 std::bitset 调用 set 和 flip 时?

由于std::bitset不使用动态分配,而你的bitset是一个局部变量,编译器可以很容易确定所有set和flips和counts都没有外部可见影响。结果,它optimizes them all away,你的代码基本上最终是一堆计时和打印调用。

请注意,在上面的程序集中,它甚至没有为 bitset 分配堆栈空间。整个事情基本消失得无影无踪。

boost::dynamic_bitset 使用new 动态分配其缓冲区,最终调用::operator new(),它可以是在不同翻译单元中定义的任意重载版本。所以编译器很难证明返回的缓冲区的变化在外部是不可见的。

您的 std::bitset 和 boost::dynamic_bitset 的 count 循环已被优化掉,因为编译器可以很容易地看到 count() 不会更改位集中的任何内容,并且您不使用返回值。

2) 为什么调用reset对速度有很大的负面影响 std::bitset?

我在 GCC 中检查了reset 的源代码,它调用了一个编译器内在函数__builtin_memset,并将一个指向缓冲区的指针传递给它。当您将指向堆栈变量的指针传递给外部函数时,编译器在可以删除的内容方面受到更多限制,因为变量中的更改现在可能是外部可观察到的(例如,被调用的函数可能存储了某处的指针,稍后可以窥视它)。

【讨论】:

  • 感谢您的详尽解释。这真的有助于理解发生了什么。你是怎么用 GCC 看源码的?您是否使用了-S 选项并查看了汇编代码?
  • @nick 你去看看&lt;bitset&gt;标头的内容:)
  • 我已经要求 Boost 也向 dynamic_bitset 添加小缓冲区优化。这有望解决 dynamic_bitsets 如果它们很小(但大小未知)将具有相似性能的问题。不知道它是否被捡起。
【解决方案2】:

嗯,它看起来像 T.C.有解释。

您在 bitset 上的所有设置和翻转(和计数)都已完全优化 出去。

提供了一个带有汇编代码的链接来显示这一点:assembly code

通过从三个不同的方法返回值并将它们添加到一个额外的变量中就可以了,现在这似乎是一场公平的战斗(正如 T.C. 所建议的那样)。

【讨论】:

    猜你喜欢
    • 2019-10-24
    • 2011-05-08
    • 1970-01-01
    • 1970-01-01
    • 2015-07-29
    • 1970-01-01
    • 1970-01-01
    • 2020-02-09
    相关资源
    最近更新 更多