boost:: dynamic_bitset медленнее, чем std::bitset, если только std::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

boost::dynamic_bitset кажется намного медленнее для set и flip.

Чтобы сделать это более интересным, если метод 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 не должен изменять ни один из битов. Сброс вывода none до и после reset подтверждает это.

Итак, после всего этого у меня два вопроса:

1) Почему boost::dynamic_bitset намного медленнее, чем std::bitset при вызове set и flip?

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 — код, вставленный выше.


person nick    schedule 19.08.2014    source источник
comment
@Т.С. Так это просто мои тесты слишком просты? Думаю, я не совсем уверен в последствиях этого. Я пытался читать ассемблерный код, но не совсем понял, что происходит.   -  person nick    schedule 20.08.2014
comment
Довольно много. Микробенчмарки писать сложно. В этом случае, если вы добавите код для вывода, например, bs, после каждого цикла, то set и flip, вероятно, не будут оптимизированы. Чтобы гарантировать, что вызовы count не будут оптимизированы, добавьте возвращаемое значение в переменную, которую затем распечатаете. dynamic_bitset использует выделение в куче, и компилятору гораздо сложнее доказать, что изменения, сделанные set и flip, будут невидимыми.   -  person T.C.    schedule 20.08.2014
comment
@Т.С. Попался. В этом есть смысл. Спасибо за разъяснение того, что происходит под капотом. Только что добавили кое-что из того, о чем вы упомянули, и теперь это будет честная битва! Я опубликую решение, цитируя то, что вы сказали, и отмечу его как решенное.   -  person nick    schedule 20.08.2014


Ответы (2)


1) Почему boost::dynamic_bitset намного медленнее, чем std::bitset при вызове set и flip?

Поскольку std::bitset не использует динамическое размещение, а bitset является локальной переменной, компилятор может легко определить, что все set, flip и count не имеют внешнего видимого эффекта. В результате он оптимизирует их все, и ваш код в основном заканчивается набором вызовов синхронизации и печати.

Обратите внимание, что в приведенной выше сборке даже не выделяется место в стеке для набора битов. Все это практически бесследно исчезло.

boost::dynamic_bitset динамически выделяет свой буфер с помощью new, что заканчивается вызовом ::operator new(), который может быть произвольной перегруженной версией, определенной в другой единице перевода. Поэтому компилятору очень сложно доказать, что изменения в возвращаемом буфере не видны извне.

Ваши циклы count как для std::bitset, так и для boost::dynamic_bitset оптимизированы, потому что компилятор может легко увидеть, что count() ничего не меняет в наборах битов, и вы не используете возвращаемое значение.

2) Почему вызов reset оказывает огромное негативное влияние на скорость std::bitset?

Я проверил исходный код reset в GCC, и он вызывает встроенный в компилятор __builtin_memset, передавая ему указатель на буфер. Когда вы передаете внешней функции указатель на стековую переменную, компилятор гораздо более ограничен в том, что он может удалить, поскольку изменения в переменной теперь могут наблюдаться извне (например, вызываемая функция могла хранить копию указатель где-то и что-то может заглянуть в него позже).

person T.C.    schedule 21.08.2014
comment
Спасибо за подробное объяснение. Это действительно помогает понять, что происходит. Как вы использовали GCC для просмотра исходного кода? Вы использовали опцию -S и смотрели ассемблерный код? - person nick; 21.08.2014
comment
@nick Иди посмотри на содержимое заголовка <bitset> :) - person T.C.; 21.08.2014
comment
Я попросил Boost добавить небольшую оптимизацию буфера в dynamic_bitset. Мы надеемся, что это решит проблему, связанную с тем, что у dynamic_bitsets будет аналогичная производительность, если они маленькие (пока неизвестный размер). Хотя не уверен, что его заберут. - person gast128; 27.01.2020

Ну, похоже, Т.С. имеет объяснение.

Все ваши наборы и перевороты (и подсчеты) в наборе битов полностью оптимизированы.

С кодом сборки была предоставлена ​​ссылка, чтобы показать это: код сборки

Возврат значений из трех разных методов и добавление их к дополнительной переменной сделали свое дело, и теперь, похоже, это честная борьба (как предложил TC).

person nick    schedule 20.08.2014