Newer bits on head chunk in DynamicBitset

Reviewers: florijan, buda

Reviewed By: florijan

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D813
This commit is contained in:
Mislav Bradac 2017-09-20 16:15:16 +02:00
parent 182a9241cf
commit 0eceefb2d4
3 changed files with 108 additions and 124 deletions

View File

@ -6,208 +6,185 @@
#include "threading/sync/spinlock.hpp" #include "threading/sync/spinlock.hpp"
#include "utils/assert.hpp" #include "utils/assert.hpp"
/** A sequentially ordered non-unique /**
* lock-free concurrent collection of bits. * A sequentially ordered non-unique lock-free concurrent collection of bits.
* *
* Grows dynamically to accomodate the maximum * Grows dynamically to accomodate the maximum set-bit position. Does not
* set-bit position. Does not dynamically * dynamically decrease in size.
* decrease in size.
* *
* Bits can be set, retrieved and cleared * Bits can be set, retrieved and cleared in groups. Note that all group
* in groups. Note that all group operations * operations fail if the group spans multiple basic storage units. For example,
* fail if the group spans multiple basic * if basic storage is in 8 bits, calling at(5, 2) is legal, but calling at(7,
* storage units. For example, if basic storage * 2) is not.
* is in 8 bits, calling at(5, 2) is legal,
* but calling at(7, 2) is not.
* *
* Organizes bits into chunks. Finding the * @tparam block_t - Basic storage type. Must have lock-free atomic. Should
* right chunk has has O(n) lookup performance, * probably always be uint_8.
* 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 * @tparam chunk_size - number of bits in one chunk
*/ */
template <class block_t = uint8_t, size_t chunk_size = 32768> template <class block_t = uint8_t, size_t chunk_size = 32768>
class DynamicBitset : Lockable<SpinLock> { class DynamicBitset {
// basic storage unit // Basic storage unit.
struct Block { struct Block {
Block() = default; Block() = default;
// the number of bits in one Block Block(const Block &) = delete;
static constexpr size_t size = sizeof(block_t) * 8; Block(Block &&) = delete;
Block &operator=(const Block &) = delete;
Block &operator=(Block &&) = delete;
// The number of bits in one Block.
static constexpr size_t kSize = sizeof(block_t) * 8;
block_t at(size_t k, size_t n) const { block_t at(size_t k, size_t n) const {
debug_assert(k + n - 1 < size, "Invalid index."); debug_assert(k + n - 1 < kSize, "Invalid index.");
return (block.load() >> k) & bitmask(n); return (block_.load() >> k) & bitmask(n);
} }
void set(size_t k, size_t n) { void set(size_t k, size_t n) {
debug_assert(k + n - 1 < size, "Invalid index."); debug_assert(k + n - 1 < kSize, "Invalid index.");
block.fetch_or(bitmask(n) << k); block_.fetch_or(bitmask(n) << k);
} }
void clear(size_t k, size_t n) { void clear(size_t k, size_t n) {
debug_assert(k + n - 1 < size, "Invalid index."); debug_assert(k + n - 1 < kSize, "Invalid index.");
block.fetch_and(~(bitmask(n) << k)); block_.fetch_and(~(bitmask(n) << k));
} }
private: private:
std::atomic<block_t> block{0}; std::atomic<block_t> block_{0};
constexpr block_t bitmask(size_t group_size) const { constexpr block_t bitmask(size_t group_size) const {
return (block_t)(-1) >> (size - group_size); return (block_t)(-1) >> (kSize - group_size);
} }
}; };
struct Chunk { struct Chunk {
Chunk() : next(nullptr) { Chunk(Chunk *next, int64_t chunk_id) : next_(next), chunk_id_(chunk_id) {
static_assert(chunk_size % sizeof(block_t) == 0, static_assert(chunk_size % Block::kSize == 0,
"chunk size not divisible by block size"); "chunk size not divisible by block size");
} }
Chunk(Chunk &) = delete; Chunk(const Chunk &) = delete;
Chunk(Chunk &&) = delete; Chunk(Chunk &&) = delete;
Chunk &operator=(const Chunk &) = delete;
Chunk &operator=(Chunk &&) = delete;
// the number of bits in one chunk // The number of bits in one chunk.
static constexpr size_t size = chunk_size * Block::size; static constexpr size_t kSize = chunk_size;
// the number of blocks in one chunk // The number of blocks_ in one chunk.
static constexpr size_t n_blocks = chunk_size / sizeof(block_t); static constexpr size_t kNumBlocks = chunk_size / Block::kSize;
block_t at(size_t k, size_t n) const { block_t at(size_t k, size_t n) const {
return blocks[k / Block::size].at(k % Block::size, n); return blocks_[k / Block::kSize].at(k % Block::kSize, n);
} }
void set(size_t k, size_t n) { void set(size_t k, size_t n) {
blocks[k / Block::size].set(k % Block::size, n); blocks_[k / Block::kSize].set(k % Block::kSize, n);
} }
void clear(size_t k, size_t n) { void clear(size_t k, size_t n) {
blocks[k / Block::size].clear(k % Block::size, n); blocks_[k / Block::kSize].clear(k % Block::kSize, n);
} }
Block blocks[n_blocks]; // Range of the bits stored in this chunk is [low, high>.
std::atomic<Chunk *> next; int64_t low() const { return chunk_id_ * kSize; }
int64_t high() const { return (chunk_id_ + 1) * kSize; }
Block blocks_[kNumBlocks];
std::atomic<Chunk *> next_;
const int64_t chunk_id_;
}; };
public: public:
DynamicBitset(){}; DynamicBitset() {}
// can't move nor copy a DynamicBitset because of atomic // Can't move nor copy a DynamicBitset because of atomic head and locking.
// head and locking
DynamicBitset(const DynamicBitset &) = delete; DynamicBitset(const DynamicBitset &) = delete;
DynamicBitset(DynamicBitset &&) = delete; DynamicBitset(DynamicBitset &&) = delete;
DynamicBitset &operator=(const DynamicBitset &) = delete; DynamicBitset &operator=(const DynamicBitset &) = delete;
DynamicBitset &operator=(DynamicBitset &&) = delete; DynamicBitset &operator=(DynamicBitset &&) = delete;
~DynamicBitset() { ~DynamicBitset() {
auto now = head.load(); auto now = head_.load();
while (now != nullptr) { while (now != nullptr) {
auto next = now->next.load(); auto next = now->next_.load();
delete now; delete now;
now = next; now = next;
} }
} }
/** Gets the block of bit starting at bit k /**
* and containing the the following n bits. * Gets the block of bit starting at bit k and containing the the following n
* The bit index with k in this bitset is * bits. The bit index with k in this bitset is zeroth bit in the returned
* zeroth bit in the returned value. * value.
*/ */
block_t at(size_t k, size_t n) const { block_t at(size_t k, size_t n) const {
if (k >= Chunk::size * chunk_count) return 0; if (k >= head_.load()->high()) return 0;
auto &chunk = find_chunk(k); const auto &chunk = FindChunk(k);
return chunk.at(k, n); return chunk.at(k, n);
} }
/** Returns k-th bit's value. */ /** Returns k-th bit's value. */
bool at(size_t k) const { return at(k, 1); } bool at(size_t k) const { return at(k, 1); }
/** Set all the bits in the group of size `n`, starting from /**
* bit `k`. * Set all the bits in the group of size `n`, starting from bit `k`.
*/ */
void set(size_t k, size_t n = 1) { void set(size_t k, size_t n = 1) {
auto &chunk = find_or_create_chunk(k); auto &chunk = FindOrCreateChunk(k);
return chunk.set(k, n); return chunk.set(k, n);
} }
/** Clears all the bits in the group of size `n`, starting /**
* from bit `k`. * Clears all the bits in the group of size `n`, starting from bit `k`.
* */ */
void clear(size_t k, size_t n = 1) { void clear(size_t k, size_t n = 1) {
// if desired bit is out of bounds, it's already clear // If desired bit is out of bounds, it's already clear.
if (k >= Chunk::size * chunk_count) return; if (k >= head_.load()->high()) return;
auto &chunk = find_or_create_chunk(k); auto &chunk = FindOrCreateChunk(k);
return chunk.clear(k, n); return chunk.clear(k, n);
} }
private: private:
// finds the chunk to which k-th bit belong. if k is // Finds the chunk to which k-th bit belongs fails if k is out of bounds.
// out of bounds, the chunk for it is created const Chunk &FindChunk(size_t &k) const {
Chunk &find_or_create_chunk(size_t &k) { debug_assert(k < head_.load()->high(), "Index out of bounds");
Chunk *chunk = head.load(), *next = nullptr; Chunk *chunk = head_;
// while i'm not in the right chunk while (k < chunk->low()) {
// (my index is bigger than the size of this chunk) chunk = chunk->next_;
while (k >= Chunk::size) { debug_assert(chunk != nullptr, "chunk is nullptr");
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 and we need it. take an exclusive
// lock to prevent others that also want to create a new chunk
// from creating it
auto guard = acquire_unique();
// double-check locking. if the chunk exists now, some other thread
// has just created it, continue searching for my chunk
if (chunk->next.load() != nullptr) continue;
chunk->next.store(new Chunk());
chunk_count++;
} }
k -= chunk->low();
debug_assert(chunk != nullptr, "Chunk is nullptr.");
return *chunk; return *chunk;
} }
// finds the chunk to which k-th bit belongs. /**
// fails if k is out of bounds * Finds the chunk to which k-th bit belong. If k is out of bounds, the chunk
const Chunk &find_chunk(size_t &k) const { * for it is created.
debug_assert(k < chunk_size * Chunk::size, "Index out of bounds"); */
Chunk *chunk = head.load(), *next = nullptr; Chunk &FindOrCreateChunk(size_t &k) {
Chunk *head = head_;
// while i'm not in the right chunk while (k >= head->high()) {
// (my index is bigger than the size of this chunk) // The next chunk does not exist and we need it, so we will try to create
while (k >= Chunk::size) { // it.
next = chunk->next.load(); Chunk *new_head = new Chunk(head, head->chunk_id_ + 1);
if (!head_.compare_exchange_strong(head, new_head)) {
// if a next chunk exists, switch to it and decrement my // Other thread updated head_ before us, so we need to delete new_head.
// pointer by the size of the current chunk head = head_;
if (next != nullptr) { delete new_head;
chunk = next;
k -= Chunk::size;
continue; continue;
} }
// the next chunk does not exist, this is illegal state
permanent_fail("Out of bounds");
} }
debug_assert(chunk != nullptr, "Chunk is nullptr."); // Now we are sure chunk exists and we can call find function.
return *chunk; // const_cast is used to avoid code duplication.
return const_cast<Chunk &>(FindChunk(k));
} }
std::atomic<Chunk *> head{new Chunk()}; std::atomic<Chunk *> head_{new Chunk(nullptr, 0)};
std::atomic<int64_t> chunk_count{1};
}; };

View File

@ -52,9 +52,6 @@ class CommitLog {
Info fetch_info(transaction_id_t id) const { return Info{log.at(2 * id, 2)}; } Info fetch_info(transaction_id_t id) const { return Info{log.at(2 * id, 2)}; }
// TODO: Searching the log will take more and more time the more and more
// transactoins are done. This could be awerted if DynamicBitset is changed
// to point to largest chunk instead of the smallest.
DynamicBitset<uint8_t, 32768> log; DynamicBitset<uint8_t, 32768> log;
}; };
} }

View File

@ -1,9 +1,18 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h" #include "gtest/gtest.h"
#include "data_structures/bitset/dynamic_bitset.hpp" #include "data_structures/bitset/dynamic_bitset.hpp"
TEST(DynamicBitset, BasicAtAndSet) { namespace {
DynamicBitset<> db;
template <typename T>
class DynamicBitsetTest : public ::testing::Test {};
typedef ::testing::Types<DynamicBitset<>, DynamicBitset<uint8_t, 8>>
DynamicBitsetTypes;
TYPED_TEST_CASE(DynamicBitsetTest, DynamicBitsetTypes);
TYPED_TEST(DynamicBitsetTest, BasicAtAndSet) {
TypeParam db;
EXPECT_EQ(db.at(17, 1), 0); EXPECT_EQ(db.at(17, 1), 0);
EXPECT_EQ(db.at(17), false); EXPECT_EQ(db.at(17), false);
@ -12,8 +21,8 @@ TEST(DynamicBitset, BasicAtAndSet) {
EXPECT_EQ(db.at(17), true); EXPECT_EQ(db.at(17), true);
} }
TEST(DynamicBitset, GroupAt) { TYPED_TEST(DynamicBitsetTest, GroupAt) {
DynamicBitset<> db; TypeParam db;
db.set(0, 1); db.set(0, 1);
db.set(1, 1); db.set(1, 1);
@ -27,8 +36,8 @@ TEST(DynamicBitset, GroupAt) {
EXPECT_EQ(db.at(1, 3), 1 | 4); EXPECT_EQ(db.at(1, 3), 1 | 4);
} }
TEST(DynamicBitset, GroupSet) { TYPED_TEST(DynamicBitsetTest, GroupSet) {
DynamicBitset<> db; TypeParam db;
EXPECT_EQ(db.at(0, 3), 0); EXPECT_EQ(db.at(0, 3), 0);
db.set(1, 2); db.set(1, 2);
EXPECT_FALSE(db.at(0)); EXPECT_FALSE(db.at(0));
@ -78,3 +87,4 @@ TEST(DynamicBitset, ConstBitset) {
dbs.set(17); dbs.set(17);
const_accepting(dbs); const_accepting(dbs);
} }
}