DynamicBitset - const correctness, tests, docs
Reviewers: dtomicevic, buda Reviewed By: buda Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D453
This commit is contained in:
parent
47b8a48df2
commit
c12990ae33
@ -6,36 +6,60 @@
|
||||
#include "threading/sync/spinlock.hpp"
|
||||
#include "utils/assert.hpp"
|
||||
|
||||
/** A sequentially ordered non-unique
|
||||
* lock-free concurrent collection of bits.
|
||||
*
|
||||
* Grows dynamically to accomodate the maximum
|
||||
* set-bit position. Does not dynamically
|
||||
* decrease in size.
|
||||
*
|
||||
* Bits can be set, retrieved and cleared
|
||||
* in groups. Note that all group operations
|
||||
* fail if the group spans multiple basic
|
||||
* storage units. For example, if basic storage
|
||||
* is in 8 bits, calling at(5, 2) is legal,
|
||||
* but calling at(7, 2) is not.
|
||||
*
|
||||
* Organizes bits into chunks. Finding the
|
||||
* right chunk has has O(n) lookup performance,
|
||||
* so use large chunks for better speed in large
|
||||
* bitsets. At the same time larger chunks
|
||||
* mean coarser-grained memory allocation.
|
||||
*
|
||||
* @tparam block_t - Basic storage type. Must have
|
||||
* lock-free atomic. Should probably always be uint_8.
|
||||
* @tparam chunk_size - number of bits in one chunk
|
||||
*/
|
||||
template <class block_t = uint8_t, size_t chunk_size = 32768>
|
||||
class DynamicBitset : Lockable<SpinLock> {
|
||||
// basic storage unit
|
||||
struct Block {
|
||||
Block() = default;
|
||||
|
||||
Block(Block &) = delete;
|
||||
Block(Block &&) = delete;
|
||||
|
||||
// the number of bits in one Block
|
||||
static constexpr size_t size = sizeof(block_t) * 8;
|
||||
|
||||
block_t at(size_t k, size_t n) const {
|
||||
debug_assert(k + n - 1 < size, "Invalid index.");
|
||||
return (block.load() >> k) & bitmask(n);
|
||||
}
|
||||
|
||||
void set(size_t k, size_t n) {
|
||||
debug_assert(k + n - 1 < size, "Invalid index.");
|
||||
block.fetch_or(bitmask(n) << k);
|
||||
}
|
||||
|
||||
void clear(size_t k, size_t n) {
|
||||
debug_assert(k + n - 1 < size, "Invalid index.");
|
||||
block.fetch_and(~(bitmask(n) << k));
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<block_t> block{0};
|
||||
|
||||
constexpr block_t bitmask(size_t group_size) const {
|
||||
return (block_t)(-1) >> (size - group_size);
|
||||
}
|
||||
|
||||
block_t at(size_t k, size_t n, std::memory_order order) {
|
||||
debug_assert(k + n - 1 < size, "Invalid index.");
|
||||
return (block.load(order) >> k) & bitmask(n);
|
||||
}
|
||||
|
||||
void set(size_t k, size_t n, std::memory_order order) {
|
||||
debug_assert(k + n - 1 < size, "Invalid index.");
|
||||
block.fetch_or(bitmask(n) << k, order);
|
||||
}
|
||||
|
||||
void clear(size_t k, size_t n, std::memory_order order) {
|
||||
debug_assert(k + n - 1 < size, "Invalid index.");
|
||||
block.fetch_and(~(bitmask(n) << k), order);
|
||||
}
|
||||
|
||||
std::atomic<block_t> block{0};
|
||||
};
|
||||
|
||||
struct Chunk {
|
||||
@ -47,19 +71,21 @@ class DynamicBitset : Lockable<SpinLock> {
|
||||
Chunk(Chunk &) = delete;
|
||||
Chunk(Chunk &&) = delete;
|
||||
|
||||
// the number of bits in one chunk
|
||||
static constexpr size_t size = chunk_size * Block::size;
|
||||
// the number of blocks in one chunk
|
||||
static constexpr size_t n_blocks = chunk_size / sizeof(block_t);
|
||||
|
||||
block_t at(size_t k, size_t n, std::memory_order order) {
|
||||
return blocks[k / Block::size].at(k % Block::size, n, order);
|
||||
block_t at(size_t k, size_t n) const {
|
||||
return blocks[k / Block::size].at(k % Block::size, n);
|
||||
}
|
||||
|
||||
void set(size_t k, size_t n, std::memory_order order) {
|
||||
blocks[k / Block::size].set(k % Block::size, n, order);
|
||||
void set(size_t k, size_t n) {
|
||||
blocks[k / Block::size].set(k % Block::size, n);
|
||||
}
|
||||
|
||||
void clear(size_t k, size_t n, std::memory_order order) {
|
||||
blocks[k / Block::size].clear(k % Block::size, n, order);
|
||||
void clear(size_t k, size_t n) {
|
||||
blocks[k / Block::size].clear(k % Block::size, n);
|
||||
}
|
||||
|
||||
Block blocks[n_blocks];
|
||||
@ -67,10 +93,14 @@ class DynamicBitset : Lockable<SpinLock> {
|
||||
};
|
||||
|
||||
public:
|
||||
DynamicBitset() : head(new Chunk()) {}
|
||||
DynamicBitset(){};
|
||||
|
||||
DynamicBitset(DynamicBitset &) = delete;
|
||||
// can't move nor copy a DynamicBitset because of atomic
|
||||
// head and locking
|
||||
DynamicBitset(const DynamicBitset &) = delete;
|
||||
DynamicBitset(DynamicBitset &&) = delete;
|
||||
DynamicBitset &operator=(const DynamicBitset &) = delete;
|
||||
DynamicBitset &operator=(DynamicBitset &&) = delete;
|
||||
|
||||
~DynamicBitset() {
|
||||
auto now = head.load();
|
||||
@ -81,28 +111,44 @@ class DynamicBitset : Lockable<SpinLock> {
|
||||
}
|
||||
}
|
||||
|
||||
block_t at(size_t k, size_t n) {
|
||||
/** Gets the block of bit starting at bit k
|
||||
* and containing the the following n bits.
|
||||
* The bit index with k in this bitset is
|
||||
* zeroth bit in the returned value.
|
||||
*/
|
||||
block_t at(size_t k, size_t n) const {
|
||||
if (k >= Chunk::size * chunk_count) return 0;
|
||||
|
||||
auto &chunk = find_chunk(k);
|
||||
return chunk.at(k, n, std::memory_order_seq_cst);
|
||||
return chunk.at(k, n);
|
||||
}
|
||||
|
||||
bool at(size_t k) {
|
||||
auto &chunk = find_chunk(k);
|
||||
return chunk.at(k, 1, std::memory_order_seq_cst);
|
||||
}
|
||||
/** Returns k-th bit's value. */
|
||||
bool at(size_t k) const { return at(k, 1); }
|
||||
|
||||
/** Set all the bits in the group of size `n`, starting from
|
||||
* bit `k`.
|
||||
*/
|
||||
void set(size_t k, size_t n = 1) {
|
||||
auto &chunk = find_chunk(k);
|
||||
return chunk.set(k, n, std::memory_order_seq_cst);
|
||||
auto &chunk = find_or_create_chunk(k);
|
||||
return chunk.set(k, n);
|
||||
}
|
||||
|
||||
/** Clears all the bits in the group of size `n`, starting
|
||||
* from bit `k`.
|
||||
* */
|
||||
void clear(size_t k, size_t n = 1) {
|
||||
auto &chunk = find_chunk(k);
|
||||
return chunk.clear(k, n, std::memory_order_seq_cst);
|
||||
// if desired bit is out of bounds, it's already clear
|
||||
if (k >= Chunk::size * chunk_count) return;
|
||||
|
||||
auto &chunk = find_or_create_chunk(k);
|
||||
return chunk.clear(k, n);
|
||||
}
|
||||
|
||||
private:
|
||||
Chunk &find_chunk(size_t &k) {
|
||||
// finds the chunk to which k-th bit belong. if k is
|
||||
// out of bounds, the chunk for it is created
|
||||
Chunk &find_or_create_chunk(size_t &k) {
|
||||
Chunk *chunk = head.load(), *next = nullptr;
|
||||
|
||||
// while i'm not in the right chunk
|
||||
@ -128,11 +174,40 @@ class DynamicBitset : Lockable<SpinLock> {
|
||||
if (chunk->next.load() != nullptr) continue;
|
||||
|
||||
chunk->next.store(new Chunk());
|
||||
chunk_count++;
|
||||
}
|
||||
|
||||
debug_assert(chunk != nullptr, "Chunk is nullptr.");
|
||||
return *chunk;
|
||||
}
|
||||
|
||||
std::atomic<Chunk *> head;
|
||||
// finds the chunk to which k-th bit belongs.
|
||||
// fails if k is out of bounds
|
||||
const Chunk &find_chunk(size_t &k) const {
|
||||
debug_assert(k < chunk_size * Chunk::size, "Index out of bounds");
|
||||
Chunk *chunk = head.load(), *next = nullptr;
|
||||
|
||||
// while i'm not in the right chunk
|
||||
// (my index is bigger than the size of this chunk)
|
||||
while (k >= Chunk::size) {
|
||||
next = chunk->next.load();
|
||||
|
||||
// if a next chunk exists, switch to it and decrement my
|
||||
// pointer by the size of the current chunk
|
||||
if (next != nullptr) {
|
||||
chunk = next;
|
||||
k -= Chunk::size;
|
||||
continue;
|
||||
}
|
||||
|
||||
// the next chunk does not exist, this is illegal state
|
||||
permanent_fail("Out of bounds");
|
||||
}
|
||||
|
||||
debug_assert(chunk != nullptr, "Chunk is nullptr.");
|
||||
return *chunk;
|
||||
}
|
||||
|
||||
std::atomic<Chunk *> head{new Chunk()};
|
||||
std::atomic<int64_t> chunk_count{1};
|
||||
};
|
||||
|
@ -2,22 +2,88 @@
|
||||
|
||||
#include "data_structures/bitset/dynamic_bitset.hpp"
|
||||
|
||||
TEST(DynamicBitset, BasicFunctionality) {
|
||||
TEST(DynamicBitset, BasicAtAndSet) {
|
||||
DynamicBitset<> db;
|
||||
db.set(222555, 1);
|
||||
bool value = db.at(222555, 1);
|
||||
ASSERT_EQ(value, true);
|
||||
|
||||
db.set(32, 1);
|
||||
value = db.at(32, 1);
|
||||
ASSERT_EQ(value, true);
|
||||
|
||||
db.clear(32, 1);
|
||||
value = db.at(32, 1);
|
||||
ASSERT_EQ(value, false);
|
||||
EXPECT_EQ(db.at(17, 1), 0);
|
||||
EXPECT_EQ(db.at(17), false);
|
||||
db.set(17, 1);
|
||||
EXPECT_EQ(db.at(17, 1), 1);
|
||||
EXPECT_EQ(db.at(17), true);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
TEST(DynamicBitset, GroupAt) {
|
||||
DynamicBitset<> db;
|
||||
|
||||
db.set(0, 1);
|
||||
db.set(1, 1);
|
||||
EXPECT_EQ(db.at(0, 2), 1 | 2);
|
||||
db.set(3, 1);
|
||||
EXPECT_EQ(db.at(0, 2), 1 | 2);
|
||||
EXPECT_EQ(db.at(0, 3), 1 | 2);
|
||||
EXPECT_EQ(db.at(0, 4), 1 | 2 | 8);
|
||||
EXPECT_EQ(db.at(1, 1), 1);
|
||||
EXPECT_EQ(db.at(1, 2), 1);
|
||||
EXPECT_EQ(db.at(1, 3), 1 | 4);
|
||||
}
|
||||
|
||||
TEST(DynamicBitset, GroupSet) {
|
||||
DynamicBitset<> db;
|
||||
EXPECT_EQ(db.at(0, 3), 0);
|
||||
db.set(1, 2);
|
||||
EXPECT_FALSE(db.at(0));
|
||||
EXPECT_TRUE(db.at(1));
|
||||
EXPECT_TRUE(db.at(2));
|
||||
EXPECT_FALSE(db.at(3));
|
||||
}
|
||||
|
||||
class Clear : public ::testing::Test {
|
||||
protected:
|
||||
DynamicBitset<> db;
|
||||
|
||||
void SetUp() override {
|
||||
db.set(17, 1);
|
||||
db.set(18, 1);
|
||||
EXPECT_EQ(db.at(17), true);
|
||||
EXPECT_EQ(db.at(18), true);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(Clear, OneElement) {
|
||||
db.clear(17, 1);
|
||||
EXPECT_EQ(db.at(17), false);
|
||||
EXPECT_EQ(db.at(18), true);
|
||||
}
|
||||
|
||||
TEST_F(Clear, Group) {
|
||||
db.clear(17, 2);
|
||||
EXPECT_EQ(db.at(17), false);
|
||||
EXPECT_EQ(db.at(18), false);
|
||||
}
|
||||
|
||||
TEST_F(Clear, EmptyGroup) {
|
||||
db.clear(17, 0);
|
||||
EXPECT_EQ(db.at(17), true);
|
||||
EXPECT_EQ(db.at(18), true);
|
||||
}
|
||||
|
||||
TEST(DynamicBitset, ConstBitset) {
|
||||
auto const_accepting = [](const DynamicBitset<> &cdbs) {
|
||||
EXPECT_FALSE(cdbs.at(16));
|
||||
EXPECT_TRUE(cdbs.at(17));
|
||||
EXPECT_FALSE(cdbs.at(18));
|
||||
};
|
||||
|
||||
DynamicBitset<> dbs;
|
||||
dbs.set(17);
|
||||
const_accepting(dbs);
|
||||
}
|
||||
|
||||
TEST(DynamicBitset, GroupAcrossBlockFail) {
|
||||
DynamicBitset<uint8_t> db;
|
||||
// groups must be aligned to block_t
|
||||
db.set(8, 1);
|
||||
EXPECT_DEATH(db.at(7, 2), "Invalid index");
|
||||
EXPECT_DEATH(db.set(7, 2), "Invalid index");
|
||||
EXPECT_DEATH(db.clear(7, 2), "Invalid index");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user