带有和不带边界检查的`std :: bitset`

时间:2014-01-31 20:31:23

标签: c++ performance stl bitset

再次关于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

1 个答案:

答案 0 :(得分:3)

当我从时间中删除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不必执行的参考代理执行TON工作。 Set有时较慢的原因是代理很简单,在这种情况下,唯一的区别是Set必须进行边界检查。另一方面,如果编译器无法证明索引总是在范围内,Set只需要进行边界检查。所以它取决于周围的代码,很多。您的结果可能会有所不同。

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

  

将位置pos处的位设置为值   如果pos与bitset中的有效位置不对应,则抛出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 []返回的l值。对通过std :: bitset :: reference发生的bitset的任何读取或写入都可能读取或写入整个基础bitset。

应该很清楚operator[]实际上比直觉更多。