ATLAS Offline Software
|
Variable-sized bitset allowing (mostly) concurrent access. More...
#include <ConcurrentBitset.h>
Classes | |
struct | const_iterator |
Iterator over all 1 bits in the set. More... | |
class | Impl |
Implementation object. More... | |
class | reference |
A reference to one bit in a set. More... | |
Public Types | |
typedef size_t | bit_t |
A bit number. More... | |
Private Types | |
typedef unsigned long | Block_t |
Internal type used to hold the bitset data. More... | |
Static Private Attributes | |
static const size_t | BLOCKSIZE = sizeof(Block_t) * CHAR_BIT |
Size, in bits, of Block_t . More... | |
static const size_t | MASK = BLOCKSIZE-1 |
Mask to select out the bit offset within one Block_t . More... | |
Constructors, destructors, assignment. | |
ConcurrentBitset (bit_t nbits=0) | |
Constructor. More... | |
ConcurrentBitset (const ConcurrentBitset &other) | |
Copy constructor. More... | |
ConcurrentBitset (std::initializer_list< bit_t > l, bit_t nbits=0) | |
Constructor from an initializer list. More... | |
ConcurrentBitset (ConcurrentBitset &&other) | |
Move constructor. More... | |
~ConcurrentBitset () | |
Destructor. More... | |
ConcurrentBitset & | operator= (const ConcurrentBitset &other) |
Assignment. More... | |
ConcurrentBitset & | operator= (ConcurrentBitset &&other) |
Move. More... | |
void | emptyGarbage () |
Clean up old versions of the set. More... | |
Implementation. | |
typedef std::mutex | mutex_t |
Mutex used for synchronization when switching to a new implementation object. More... | |
typedef std::lock_guard< mutex_t > | lock_t |
std::atomic< Impl * > | m_impl |
The current implementation object. More... | |
std::vector< Impl * > | m_garbage |
Old implementation objects, pending deletion. More... | |
mutex_t | m_mutex |
static bit_t | nBlocks (bit_t nbits) |
Find number of blocks needed to hold a given number of bits. More... | |
Impl * | newImpl (bit_t nbits) |
Create a new, uninitialized implementation object. More... | |
void | expand (bit_t new_nbits) |
Expand the container. More... | |
void | expandOol (bit_t new_nbits) |
Expand the container: out-of-line portion. More... | |
Variable-sized bitset allowing (mostly) concurrent access.
This class represents a set of bits. One can think of such an object as either like a std::bitset or like a std::set<unsigned>. This class provides basically the union of the two interfaces. In a few cases, the same method has different semantics in the two interfaces, most notably size()
. We follow here the semantics that we would get from std::set<unsigned>. (Rationale: The motivation for this class is to replace the existing use of unordered_set<unsigned> for auxid_set_t. Supplying the same interface makes this easier to drop in as a replacement. However, in some cases, the set operations as provided by a bitset-like interface can be much more efficient, so we'd want to provide those as well.)
The size of the bitset is specified at runtime, to the constructor. Most operations do not change the size. However, the insert()
methods, as well as operator=
, may be used to grow the size of the set.
Most methods can execute completely concurrently. However, the methods which can change the size of the container, insert()
and operator=
, are not compatible with other concurrent writes. (Concurrent reads are ok, though.)
Some methods can operate on the entire set (e.g., clear()
). Such methods may run concurrently with others, but they will not necessarily be atomic: other threads may be able to see the operation partially completed.
An iterator is provided that iterates over all bits that are set (like iterating over a set<unsigned>).
When the container is expanded, the internal representation needs to be reallocated. The old object is not, however, deleted immediately, as other threads may still be referencing it. If if some point you know that no threads can be referencing old versions any more, you can clean them up by calling emptyGarbage
(otherwise, all versions will be deleted when the set object is destroyed).
Some notes on motivation:
The use case that motivated this class was auxid_set_t, which tells which auxiliary variables are available for a given container. This is logically a relatively sparse set of relatively small integers. (Maximum value is ~2000, with a set having ~100 entries.)
This had been implemented as a std::unordered_set<size_t>, but this was not entirely satisfactory. First, in some cases, there was a significant overhead in inserting and deleting items from the set. Second, unordered_set does not allow concurrent reading and writing. This meant that we ended up maintaining thread-local copies of each set, which adds extra overhead and complexity.
The threading issue suggests that we would like to use a container that allows readers to run currently with at least one writer. The fact that the maximum value to be stored in these sets is not too large suggests that a bitmap might be a good representation, as long as the time required to iterate over the set doesn't blow up due to the sparseness of the map.
To study this, a reconstruction job was instrumented to dump out all unique auxid_set_t values. This corpus was then used to run a set of timing tests for different set implementations. This test is built into the ConcurrentBitset unit test, and the corpus is available in the CxxUtils package. Run like:
Set implementations that have been tested so far include:
Representative results (times in seconds; lower is better):
For this workload, the bitmaps are the clear winner.
Implementation notes:
The implementation here is inspired by ck_bitmap from ConcurrencyKit (http://concurrencykit.org/), though the code is all new. ck_bitmap itself isn't suitable because it doesn't allow for the set to grow, and also because it's a pure C interface, while we want something with a C++ style interface (and in particular something which supports interfaces close to unordered_set<size_t> in order to ease migration).
The bitset is stored as an array of std::atomic<Block_t> objects, where Block_t is the largest unsigned type for which atomic is lockless. This is stored in a separate implementation object in order to allow the container to grow. The implementation object has a fixed-size header followed by the variable-size array of blocks.
To speed up iteration for the typical case of a sparse set, we maintain a ‘high-water’ mark, m_hwm, which is the index of the highest block that might have a bit set. m_hwm can only increase unless the set is cleared.
Definition at line 145 of file ConcurrentBitset.h.
typedef size_t CxxUtils::ConcurrentBitset::bit_t |
A bit number.
Definition at line 168 of file ConcurrentBitset.h.
|
private |
Internal type used to hold the bitset data.
The bitset is an array of std::atomic<Block_t>. This type should generally be the largest unsigned type for which std::atomic is lockless.
Definition at line 155 of file ConcurrentBitset.h.
|
private |
Definition at line 1075 of file ConcurrentBitset.h.
|
private |
Mutex used for synchronization when switching to a new implementation object.
Definition at line 1074 of file ConcurrentBitset.h.
CxxUtils::ConcurrentBitset::ConcurrentBitset | ( | bit_t | nbits = 0 | ) |
Constructor.
nbits | Initial number of bits to allocate for the map. |
Definition at line 26 of file ConcurrentBitset.cxx.
CxxUtils::ConcurrentBitset::ConcurrentBitset | ( | const ConcurrentBitset & | other | ) |
Copy constructor.
other | Container to copy. |
The copy is not atomic. If a non-atomic update is simultaneously made to other
, then the copy may have this update only partially completed.
Definition at line 39 of file ConcurrentBitset.cxx.
Constructor from an initializer list.
List | of values to set. |
nbits | Number of bits to allocate for the map. If 0, then set the size based on the maximum value in the list. |
This allows setting specific bits in the set, like
Definition at line 56 of file ConcurrentBitset.cxx.
CxxUtils::ConcurrentBitset::ConcurrentBitset | ( | ConcurrentBitset && | other | ) |
Move constructor.
other | Container to move. |
No concurrent access may be in progress on other
. After this returns, other
can only be deleted.
Definition at line 82 of file ConcurrentBitset.cxx.
CxxUtils::ConcurrentBitset::~ConcurrentBitset | ( | ) |
Destructor.
Definition at line 94 of file ConcurrentBitset.cxx.
bool CxxUtils::ConcurrentBitset::all | ( | ) | const |
Return true if all bits in the set are 1.
bool CxxUtils::ConcurrentBitset::any | ( | ) | const |
Return true if there are any 1 bits in the set.
const_iterator CxxUtils::ConcurrentBitset::begin | ( | ) | const |
Return a begin iterator.
bit_t CxxUtils::ConcurrentBitset::capacity | ( | ) | const |
The number of bits that this container can hold.
ConcurrentBitset& CxxUtils::ConcurrentBitset::clear | ( | ) |
Clear all bits in the set.
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
bit_t CxxUtils::ConcurrentBitset::count | ( | ) | const |
Count the number of 1 bits in the set.
size_t CxxUtils::ConcurrentBitset::count | ( | bit_t | bit | ) | const |
Test to see if a bit is set.
bit | Number of the bit to test. |
Returns 0 if bit
is beyond the end of the set.
bool CxxUtils::ConcurrentBitset::empty | ( | ) | const |
Return true if there are no 1 bits in the set.
void CxxUtils::ConcurrentBitset::emptyGarbage | ( | ) |
Clean up old versions of the set.
The insert and assignment operations may need to grow the set. The original version of the set is not deleted immediately, since other threads may still be accessing it. Call this when no other threads can be accessing old versions in order to clean them up.
Definition at line 148 of file ConcurrentBitset.cxx.
const_iterator CxxUtils::ConcurrentBitset::end | ( | ) | const |
Return an end iterator.
ConcurrentBitset& CxxUtils::ConcurrentBitset::erase | ( | bit_t | bit | ) |
Turn off one bit.
bit | The bit to turn off. |
Does nothing if bit
beyond the end of the set.
|
private |
Expand the container.
new_nbits | The desired new size of the container. |
|
private |
Expand the container: out-of-line portion.
new_nbits | The desired new size of the container. |
Definition at line 162 of file ConcurrentBitset.cxx.
const_iterator CxxUtils::ConcurrentBitset::find | ( | bit_t | bit | ) | const |
If bit bit
is set, return an iterator pointing to it.
Otherwise, return an end iterator.
bit | Bit number to test. |
ConcurrentBitset& CxxUtils::ConcurrentBitset::flip | ( | ) |
Flip the state of all bits in the set.
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset& CxxUtils::ConcurrentBitset::flip | ( | bit_t | bit | ) |
Flip the value of one bit.
bit | The bit to turn flip. |
Does nothing if bit
beyond the end of the set.
ConcurrentBitset& CxxUtils::ConcurrentBitset::insert | ( | bit_t | bit, |
bit_t | new_nbits = 0 |
||
) |
Set a bit to 1.
Expand the set if needed.
bit | Number of the bit to set. |
new_nbits | Hint for new size of set, if it needs to be expanded. |
If bit
is past the end of the container, then the container will be expanded as needed.
This operation is incompatible with any other simultaneous writes to the same set (reads are ok).
ConcurrentBitset& CxxUtils::ConcurrentBitset::insert | ( | const ConcurrentBitset & | other | ) |
Turn on bits listed in another set.
other | Set of bits to turn on. |
This is the same as operator|=
, except that if the size of other
is larger than this set, then this set will be expanded to match other
.
This operation is incompatible with any other simultaneous writes to the same set (reads are ok).
ConcurrentBitset& CxxUtils::ConcurrentBitset::insert | ( | ITERATOR | beg, |
ITERATOR | end, | ||
bit_t | new_nbits = 0 |
||
) |
Set several bits to 1.
Expand the set if needed.
beg | Start of range of bits to set. |
end | End of range of bits to set. |
new_nbits | Hint for new size of set, if it needs to be expanded. |
The iteration range should be over something convertible to bit_t
. If any bit is past the end of the container, then the container will be expanded as needed.
This operation is incompatible with any other simultaneous writes to the same set (reads are ok).
Example:
ConcurrentBitset& CxxUtils::ConcurrentBitset::insert | ( | std::initializer_list< bit_t > | l, |
bit_t | new_nbits = 0 |
||
) |
Set several bits to 1.
Expand the set if needed.
l | List of bits to set. |
new_nbits | Hint for new size of set, if it needs to be expanded. |
If any bit is past the end of the container, then the container will be expanded as needed.
This operation is incompatible with any other simultaneous writes to the same set (reads are ok).
Example:
Find number of blocks needed to hold a given number of bits.
nbits | The number of bits. |
Create a new, uninitialized implementation object.
nbits | Number of bits to allocate. |
This will allocate memory for the Impl object, but will does not run the constructor.
bool CxxUtils::ConcurrentBitset::none | ( | ) | const |
Return true if there are no 1 bits in the set.
bool CxxUtils::ConcurrentBitset::operator!= | ( | const ConcurrentBitset & | other | ) | const |
Test two sets for inequality.
other | The other set to test. |
ConcurrentBitset& CxxUtils::ConcurrentBitset::operator&= | ( | const ConcurrentBitset & | other | ) |
AND this set with another set.
other | The other set. |
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset& CxxUtils::ConcurrentBitset::operator-= | ( | const ConcurrentBitset & | other | ) |
Subtract another set from this set.
other | The other set. |
This is the same as (*this) &= ~other;
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset & CxxUtils::ConcurrentBitset::operator= | ( | ConcurrentBitset && | other | ) |
Move.
other | Bitset from which to move. |
No concurrent access may be in progress on other
. After this returns, other
can only be deleted.
Definition at line 126 of file ConcurrentBitset.cxx.
ConcurrentBitset & CxxUtils::ConcurrentBitset::operator= | ( | const ConcurrentBitset & | other | ) |
Assignment.
other | Bitset from which to assign. |
The copy is not atomic. If a non-atomic update is simultaneously made to other
, then the copy may have this update only partially completed.
Definition at line 108 of file ConcurrentBitset.cxx.
bool CxxUtils::ConcurrentBitset::operator== | ( | const ConcurrentBitset & | other | ) | const |
Test two sets for equality.
other | The other set to test. |
Return a reference to one bit.
bit | The number of the bit to reference. |
The reference will be invalidated by calls to insert()
or operator=
. Effects are undefined if bit
is past the end of the set.
bool CxxUtils::ConcurrentBitset::operator[] | ( | bit_t | bit | ) | const |
Return the value of one bit.
bit | The number of the bit to test. |
ConcurrentBitset& CxxUtils::ConcurrentBitset::operator^= | ( | const ConcurrentBitset & | other | ) |
XOR this set with another set.
other | The other set. |
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset& CxxUtils::ConcurrentBitset::operator|= | ( | const ConcurrentBitset & | other | ) |
OR this set with another set.
other | The other set. |
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset CxxUtils::ConcurrentBitset::operator~ | ( | ) | const |
Return a new set that is the complement of this set.
ConcurrentBitset& CxxUtils::ConcurrentBitset::reset | ( | ) |
Clear all bits in the set.
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset& CxxUtils::ConcurrentBitset::reset | ( | bit_t | bit | ) |
Turn off one bit.
bit | The bit to turn off. |
Does nothing if bit
beyond the end of the set.
ConcurrentBitset& CxxUtils::ConcurrentBitset::set | ( | ) |
Turn on all bits in the set.
This operation is not necessarily atomic; a simultaneous read may be able to see the operation partially done.
ConcurrentBitset& CxxUtils::ConcurrentBitset::set | ( | bit_t | bit | ) |
Turn on one bit.
bit | The bit to turn on. |
Does nothing if bit
beyond the end of the set.
ConcurrentBitset& CxxUtils::ConcurrentBitset::set | ( | bit_t | bit, |
bool | val | ||
) |
Set the value of one bit.
bit | The bit to turn set. |
val | The value to which to set it. |
Does nothing if bit
beyond the end of the set.
bit_t CxxUtils::ConcurrentBitset::size | ( | ) | const |
Count the number of 1 bits in the set.
Note: If you regard this like a std::bitset, you would expect this to return the number of bits that the set can hold, while if you regard this like a set<bit_t>, then you would expect this to return the number of 1 bits. We follow the latter here.
bool CxxUtils::ConcurrentBitset::test | ( | bit_t | bit | ) | const |
Test to see if a bit is set.
bit | Number of the bit to test. |
Returns false if bit
is beyond the end of the set.
Size, in bits, of Block_t
.
Definition at line 158 of file ConcurrentBitset.h.
|
private |
Old implementation objects, pending deletion.
Definition at line 1071 of file ConcurrentBitset.h.
|
private |
The current implementation object.
Definition at line 1068 of file ConcurrentBitset.h.
|
private |
Definition at line 1076 of file ConcurrentBitset.h.
Mask to select out the bit offset within one Block_t
.
Definition at line 161 of file ConcurrentBitset.h.