【问题标题】:`std::bitset` with and without boundary checks`std::bitset` 有和没有边界检查
【发布时间】:2014-01-31 20:31:22
【问题描述】:

关于 STL std::bitset - 它的文档说函数 set/reset/test 进行边界检查,而 operator[] 没有。我的计时实验表明,set/test 函数的执行速度通常比operator[] 快 2-3%。我正在使用的代码是:

typedef unsigned long long U64;
const U64 MAX = 800000000ULL;

struct Bitmap1
{
  void insert(U64 N) {this->s[N % MAX] = 1;}
  bool find(U64 N) const {return this->s[N % MAX];}
private:
  std::bitset<MAX> s;  // <---- takes MAX/8 memory (in bytes)
};

struct Bitmap2
{
  void insert(U64 N) {this->s.set(N % MAX);}
  bool find(U64 N) const {return this->s.test(N % MAX);}
private:
  std::bitset<MAX> s;  // <---- takes MAX/8 memory (in bytes)
};

int main()
{
  Bitmap2* s = new Bitmap2();
  // --------------------------- storing
  const size_t t0 = time(0);
  for (unsigned k = 0; k < LOOPS; ++k)
  {
    for (U64 i = 0; i < MAX; ++i) s->insert(i);
  }
  cout << "storing: " << time(0) - t0 << endl;
  // -------------------------------------- search
  const size_t t1 = time(0);
  U64 count = 0;
  for (unsigned k = 0; k < LOOPS; ++k)
  {
    for (U64 i = 0; i < MAX; ++i) if (s->find(i)) ++count;
  }
  cout << "search:  " << time(0) - t1 << endl;
  cout << count << endl;
}

如何解释?没有边界检查应该可以为我们节省一些周期,对吧?

Compiler: g++ 4.8.1 (options -g -O4)
VMware VM: Ubuntu 3.11.0-15
Host: MacBook Pro

【问题讨论】:

  • 可能是基准测试错误。请发布完整的示例。
  • 你做了哪些具体的实验?你能告诉我们你尝试了什么,你使用了什么编译器和编译器标志,以及结果吗?
  • @AlekseyYakovlev:Bitset::operator[] 必须创建和使用代理。 Bitset::Set 不需要做这样的事情。我希望Set 实际上远更快,但由于分配 100MB 数据的缓存,差异会丢失。
  • 使用 bitset 非常便宜,以至于您的测试完全由 rand 甚至 % 运算符主导,这实际上是一个非常慢的除法。这意味着您的基准测试对编译器更改有关不相关代码的内容非常敏感。您没有以有效的方式进行基准测试。首先修复基准。
  • @MooingDuck - 我的意思是二进制大小爆炸性增长导致的二阶效应,这里公式化linuxjournal.com/article/7269

标签: c++ performance stl bitset


【解决方案1】:

当我从计时中删除rand、除法、输出和内存缓存时:

bool bracket_test() {
    std::bitset<MAX> s;
    for(int j=0; j<num_iterations; ++j) {
        for(int i=0; i<MAX; ++i)
            s[i] = !s[MAX-1-i];
    }
    return s[0];
}
bool set_test() {
    std::bitset<MAX> s;
    for(int j=0; j<num_iterations; ++j) {
        for(int i=0; i<MAX; ++i)
            s.set(i, !s.test(MAX-1-i));
    }
    return s.test(0);
}
bool no_test() {
    bool s = false;
    for(int j=0; j<num_iterations; ++j) {
        for(int i=0; i<MAX; ++i)
            s = !s;
    }
    return s;
}

我在http://coliru.stacked-crooked.com/a/cdc832bfcc7e32be 使用 Clang 获得了这些结果。 (我做了 10000 次迭代,20 次,并测量了最短时间,这可以减少计时错误。)

clang++ -std=c++11  -O0 -Wall -Wextra -pedantic -pthread main.cpp  && ./a.out
bracket_test took 178663845 ticks to find result 1
set_test     took 117336632 ticks to find result 1
no_test      took 9214297 ticks to find result 0
clang++ -std=c++11  -O1 -Wall -Wextra -pedantic -pthread main.cpp  && ./a.out
bracket_test took 798184780 ticks to find result 1
set_test     took 565999680 ticks to find result 1
no_test      took 41693575 ticks to find result 0
clang++ -std=c++11  -O2 -Wall -Wextra -pedantic -pthread main.cpp  && ./a.out
bracket_test took 81240369 ticks to find result 1
set_test     took 72172912 ticks to find result 1
no_test      took 41907685 ticks to find result 0
clang++ -std=c++11  -O3 -Wall -Wextra -pedantic -pthread main.cpp  && ./a.out
bracket_test took 77688054 ticks to find result 1
set_test     took 72433185 ticks to find result 1
no_test      took 41433010 ticks to find result 0

此测试的先前版本发现括号稍微快一些,但现在我已经提高了计时的准确性,我的计时误差范围似乎约为 3%。在 O1 Set 上快 35-54%,在 O2 上快 13-49%,在 O3 上快 2-34%。除了查看程序集输出之外,这对我来说似乎很有定论。

这里是通过http://assembly.ynh.io/ 组装的(GCC -O):

std::bitset<MAX> s
s[1000000] = true;
return s;

0000 4889F8         movq    %rdi, %rax
0003 4889FA         movq    %rdi, %rdx
0006 488D8F00       leaq    100000000(%rdi), %rcx
     E1F505
000d 48C70200       movq    $0, (%rdx)
     000000
0014 4883C208       addq    $8, %rdx
0018 4839CA         cmpq    %rcx, %rdx
001b 75F0           jne .L2
001d 48838848       orq $1, 125000(%rax)
     E8010001 
0025 C3             ret

和

std::bitset<MAX> s;
s.set(1000000);
return s;

0026 4889F8         movq    %rdi, %rax
0029 4889FA         movq    %rdi, %rdx
002c 488D8F00       leaq    100000000(%rdi), %rcx
     E1F505
0033 48C70200       movq    $0, (%rdx)
     000000
003a 4883C208       addq    $8, %rdx
003e 4839CA         cmpq    %rcx, %rdx
0041 75F0           jne .L6
0043 48838848       orq $1, 125000(%rax)
     E8010001 
004b C3             ret

我真的不能很好地阅读汇编,但它们是完全相同的,所以分析这个案例很容易。如果编译器知道它们都在范围内,它会优化范围检查。当我用变量索引替换固定索引时,Set 添加了 5 个操作来检查边界情况。

至于 Set 有时更快的原因是,operator[] 必须为引用代理做大量工作,而 Set 不必做。 Set 有时较慢的原因是代理被简单地内联,在这种情况下,唯一的区别是 Set 必须进行边界检查。另一方面,Set 只需在编译器无法证明索引始终在范围内时进行边界检查。所以这取决于周围的代码,很多。您的结果可能会有所不同。

http://en.cppreference.com/w/cpp/utility/bitset/set 说:

将位置 pos 的位设置为值 value。
如果 pos 不对应于位集中的有效位置,则抛出 std::out_of_range。

http://en.cppreference.com/w/cpp/utility/bitset/operator_at 说:

访问位置 pos 的位。返回允许修改值的 std::bitset::reference 类型的对象。
与 test() 不同,它不会抛出异常:如果 pos 超出范围,则行为未定义。

和http://en.cppreference.com/w/cpp/utility/bitset/reference 说:

std::bitset 类包括 std::bitset::reference 作为可公开访问的嵌套类。此类用作代理对象以允许用户与位集的各个位进行交互,因为标准 C++ 类型(如引用和指针)没有以足够的精度构建来指定各个位。 std::bitset::reference 的主要用途是提供一个可以从 operator[] 返回的左值。通过 std::bitset::reference 对 bitset 进行的任何读取或写入都可能读取或写入整个底层 bitset。

应该清楚,operator[] 实际上比直观的要多得多。

【讨论】:

  • 所以,底线是 - 我们不应该期望operator[]总是比set/test快,对吧?如果是,那应该进入一些广为人知的关于std::bitset 我认为的文档
  • @AlekseyYakovlev:operator[] 与 Set 的相对性能与 C++ 中的其他所有内容一样,都是由实现定义的。
  • @MooingDuck - 好吧,我不同意。如果程序逻辑允许 STL 用户避免边界检查,那么 [s] 在任何优化级别上都不应该为此向他征税。
  • @AlekseyYakovlev:有趣的是,GCC 似乎没有正确优化 operator[],但 clang 确实如此。我想知道 MSVC 是做什么的。 operator[] 旨在成为避免边界检查的方式,错误 GCC 似乎无法实现。
  • @AlekseyYakovlev:我做了更准确的计时测试,并考虑了一些差异。这次似乎Set 在所有优化级别上确实更快。
猜你喜欢
  • 1970-01-01
  • 2014-03-09
  • 2019-10-19
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
  • 2012-11-13
  • 2011-11-16
相关资源
最近更新 更多