Better Pool
This commit is contained in:
parent
37f11a75d4
commit
0ab4e0fa53
@ -150,110 +150,53 @@ void *MonotonicBufferResource::DoAllocate(size_t bytes, size_t alignment) {
|
|||||||
|
|
||||||
namespace impl {
|
namespace impl {
|
||||||
|
|
||||||
Pool::Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *memory)
|
Pool::Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *chunk_memory)
|
||||||
: blocks_per_chunk_(blocks_per_chunk), block_size_(block_size), chunks_(memory) {}
|
: blocks_per_chunk_(blocks_per_chunk),
|
||||||
|
block_size_(block_size),
|
||||||
Pool::~Pool() { MG_ASSERT(chunks_.empty(), "You need to call Release before destruction!"); }
|
data_size_{blocks_per_chunk_ * block_size_},
|
||||||
|
alignment_{Ceil2(block_size_)},
|
||||||
void *Pool::Allocate() {
|
chunks_(chunk_memory) {
|
||||||
auto allocate_block_from_chunk = [this](Chunk *chunk) {
|
|
||||||
unsigned char *available_block = chunk->data + (chunk->first_available_block_ix * block_size_);
|
|
||||||
// Update free-list pointer (index in our case) by reading "next" from the
|
|
||||||
// available_block.
|
|
||||||
chunk->first_available_block_ix = *available_block;
|
|
||||||
--chunk->blocks_available;
|
|
||||||
return available_block;
|
|
||||||
};
|
|
||||||
if (last_alloc_chunk_ && last_alloc_chunk_->blocks_available > 0U)
|
|
||||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
|
||||||
// Find a Chunk with available memory.
|
|
||||||
for (auto &chunk : chunks_) {
|
|
||||||
if (chunk.blocks_available > 0U) {
|
|
||||||
last_alloc_chunk_ = &chunk;
|
|
||||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We haven't found a Chunk with available memory, so allocate a new one.
|
|
||||||
if (block_size_ > std::numeric_limits<size_t>::max() / blocks_per_chunk_) throw BadAlloc("Allocation size overflow");
|
|
||||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
|
||||||
// Use the next pow2 of block_size_ as alignment, so that we cover alignment
|
// Use the next pow2 of block_size_ as alignment, so that we cover alignment
|
||||||
// requests between 1 and block_size_. Users of this class should make sure
|
// requests between 1 and block_size_. Users of this class should make sure
|
||||||
// that requested alignment of particular blocks is never greater than the
|
// that requested alignment of particular blocks is never greater than the
|
||||||
// block itself.
|
// block itself.
|
||||||
size_t alignment = Ceil2(block_size_);
|
if (block_size_ > std::numeric_limits<size_t>::max() / blocks_per_chunk_) throw BadAlloc("Allocation size overflow");
|
||||||
if (alignment < block_size_) throw BadAlloc("Allocation alignment overflow");
|
if (alignment_ < block_size_) throw BadAlloc("Allocation alignment overflow");
|
||||||
auto *data = reinterpret_cast<unsigned char *>(GetUpstreamResource()->Allocate(data_size, alignment));
|
}
|
||||||
// Form a free-list of blocks in data.
|
|
||||||
for (unsigned char i = 0U; i < blocks_per_chunk_; ++i) {
|
|
||||||
*(data + (i * block_size_)) = i + 1U;
|
|
||||||
}
|
|
||||||
Chunk chunk{data, 0, blocks_per_chunk_};
|
|
||||||
// Insert the big block in the sorted position.
|
|
||||||
auto it = std::lower_bound(chunks_.begin(), chunks_.end(), chunk,
|
|
||||||
[](const auto &a, const auto &b) { return a.data < b.data; });
|
|
||||||
try {
|
|
||||||
it = chunks_.insert(it, chunk);
|
|
||||||
} catch (...) {
|
|
||||||
GetUpstreamResource()->Deallocate(data, data_size, alignment);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
last_alloc_chunk_ = &*it;
|
Pool::~Pool() {
|
||||||
last_dealloc_chunk_ = &*it;
|
if (!chunks_.empty()) Release();
|
||||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
DMG_ASSERT(chunks_.empty(), "You need to call Release before destruction!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void *Pool::Allocate() {
|
||||||
|
if (!free_list_) {
|
||||||
|
// need new chunk
|
||||||
|
auto *data = reinterpret_cast<std::byte *>(GetUpstreamResource()->Allocate(data_size_, alignment_));
|
||||||
|
try {
|
||||||
|
auto &new_chunk = chunks_.emplace_front(data);
|
||||||
|
free_list_ = new_chunk.build_freelist(block_size_, blocks_per_chunk_);
|
||||||
|
} catch (...) {
|
||||||
|
GetUpstreamResource()->Deallocate(data, data_size_, alignment_);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::exchange(free_list_, *reinterpret_cast<std::byte **>(free_list_));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pool::Deallocate(void *p) {
|
void Pool::Deallocate(void *p) {
|
||||||
MG_ASSERT(last_dealloc_chunk_, "No chunk to deallocate");
|
*reinterpret_cast<std::byte **>(p) = std::exchange(free_list_, reinterpret_cast<std::byte *>(p));
|
||||||
MG_ASSERT(!chunks_.empty(),
|
|
||||||
"Expected a call to Deallocate after at least a "
|
|
||||||
"single Allocate has been done.");
|
|
||||||
auto is_in_chunk = [this, p](const Chunk &chunk) {
|
|
||||||
auto ptr = reinterpret_cast<uintptr_t>(p);
|
|
||||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
|
||||||
return reinterpret_cast<uintptr_t>(chunk.data) <= ptr && ptr < reinterpret_cast<uintptr_t>(chunk.data + data_size);
|
|
||||||
};
|
|
||||||
auto deallocate_block_from_chunk = [this, p](Chunk *chunk) {
|
|
||||||
// NOTE: This check is not enough to cover all double-free issues.
|
|
||||||
MG_ASSERT(chunk->blocks_available < blocks_per_chunk_,
|
|
||||||
"Deallocating more blocks than a chunk can contain, possibly a "
|
|
||||||
"double-free situation or we have a bug in the allocator.");
|
|
||||||
// Link the block into the free-list
|
|
||||||
auto *block = reinterpret_cast<unsigned char *>(p);
|
|
||||||
*block = chunk->first_available_block_ix;
|
|
||||||
chunk->first_available_block_ix = (block - chunk->data) / block_size_;
|
|
||||||
chunk->blocks_available++;
|
|
||||||
};
|
|
||||||
if (is_in_chunk(*last_dealloc_chunk_)) {
|
|
||||||
deallocate_block_from_chunk(last_dealloc_chunk_);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the chunk which served this allocation
|
|
||||||
Chunk chunk{reinterpret_cast<unsigned char *>(p) - blocks_per_chunk_ * block_size_, 0, 0};
|
|
||||||
auto it = std::lower_bound(chunks_.begin(), chunks_.end(), chunk,
|
|
||||||
[](const auto &a, const auto &b) { return a.data <= b.data; });
|
|
||||||
MG_ASSERT(it != chunks_.end(), "Failed deallocation in utils::Pool");
|
|
||||||
MG_ASSERT(is_in_chunk(*it), "Failed deallocation in utils::Pool");
|
|
||||||
|
|
||||||
// Update last_alloc_chunk_ as well because it now has a free block.
|
|
||||||
// Additionally this corresponds with C++ pattern of allocations and
|
|
||||||
// deallocations being done in reverse order.
|
|
||||||
last_alloc_chunk_ = &*it;
|
|
||||||
last_dealloc_chunk_ = &*it;
|
|
||||||
deallocate_block_from_chunk(last_dealloc_chunk_);
|
|
||||||
// TODO: We could release the Chunk to upstream memory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pool::Release() {
|
void Pool::Release() {
|
||||||
for (auto &chunk : chunks_) {
|
auto *resource = GetUpstreamResource();
|
||||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
if (!dynamic_cast<utils::MonotonicBufferResource *>(resource)) {
|
||||||
size_t alignment = Ceil2(block_size_);
|
for (auto &chunk : chunks_) {
|
||||||
GetUpstreamResource()->Deallocate(chunk.data, data_size, alignment);
|
resource->Deallocate(chunk.raw_data, data_size_, alignment_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chunks_.clear();
|
chunks_.clear();
|
||||||
last_alloc_chunk_ = nullptr;
|
free_list_ = nullptr;
|
||||||
last_dealloc_chunk_ = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace impl
|
} // namespace impl
|
||||||
@ -262,7 +205,7 @@ PoolResource::PoolResource(size_t max_blocks_per_chunk, size_t max_block_size, M
|
|||||||
MemoryResource *memory_unpooled)
|
MemoryResource *memory_unpooled)
|
||||||
: pools_(memory_pools),
|
: pools_(memory_pools),
|
||||||
unpooled_(memory_unpooled),
|
unpooled_(memory_unpooled),
|
||||||
max_blocks_per_chunk_(std::min(max_blocks_per_chunk, static_cast<size_t>(impl::Pool::MaxBlocksInChunk()))),
|
max_blocks_per_chunk_(std::min(max_blocks_per_chunk, static_cast<size_t>(impl::Pool::MaxBlocksInChunk))),
|
||||||
max_block_size_(max_block_size) {
|
max_block_size_(max_block_size) {
|
||||||
MG_ASSERT(max_blocks_per_chunk_ > 0U, "Invalid number of blocks per chunk");
|
MG_ASSERT(max_blocks_per_chunk_ > 0U, "Invalid number of blocks per chunk");
|
||||||
MG_ASSERT(max_block_size_ > 0U, "Invalid size of block");
|
MG_ASSERT(max_block_size_ > 0U, "Invalid size of block");
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <forward_list>
|
||||||
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <new>
|
#include <new>
|
||||||
@ -382,37 +385,47 @@ class MonotonicBufferResource final : public MemoryResource {
|
|||||||
|
|
||||||
namespace impl {
|
namespace impl {
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
using AList = std::forward_list<T, Allocator<T>>;
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
using AVector = std::vector<T, Allocator<T>>;
|
using AVector = std::vector<T, Allocator<T>>;
|
||||||
|
|
||||||
/// Holds a number of Chunks each serving blocks of particular size. When a
|
/// Holds a number of Chunks each serving blocks of particular size. When a
|
||||||
/// Chunk runs out of available blocks, a new Chunk is allocated. The naming is
|
/// Chunk runs out of available blocks, a new Chunk is allocated.
|
||||||
/// taken from `libstdc++` implementation, but the implementation details are
|
|
||||||
/// more similar to `FixedAllocator` described in "Small Object Allocation" from
|
|
||||||
/// "Modern C++ Design".
|
|
||||||
class Pool final {
|
class Pool final {
|
||||||
/// Holds a pointer into a chunk of memory which consists of equal sized
|
/// Holds a pointer into a chunk of memory which consists of equal sized
|
||||||
/// blocks. Each Chunk can handle `std::numeric_limits<unsigned char>::max()`
|
/// blocks. Blocks form a "free-list"
|
||||||
/// number of blocks. Blocks form a "free-list", where each unused block has
|
|
||||||
/// an embedded index to the next unused block.
|
|
||||||
struct Chunk {
|
struct Chunk {
|
||||||
unsigned char *data;
|
// TODO: make blocks_per_chunk a per chunk thing (ie. allow chunk growth)
|
||||||
unsigned char first_available_block_ix;
|
std::byte *raw_data;
|
||||||
unsigned char blocks_available;
|
explicit Chunk(std::byte *rawData) : raw_data(rawData) {}
|
||||||
|
std::byte *build_freelist(std::size_t block_size, std::size_t blocks_in_chunk) {
|
||||||
|
auto current = raw_data;
|
||||||
|
std::byte *prev = nullptr;
|
||||||
|
auto end = current + (blocks_in_chunk * block_size);
|
||||||
|
while (current != end) {
|
||||||
|
std::byte **list_entry = reinterpret_cast<std::byte **>(current);
|
||||||
|
*list_entry = std::exchange(prev, current);
|
||||||
|
current += block_size;
|
||||||
|
}
|
||||||
|
DMG_ASSERT(prev != nullptr);
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char blocks_per_chunk_;
|
std::byte *free_list_{nullptr};
|
||||||
size_t block_size_;
|
uint8_t blocks_per_chunk_{};
|
||||||
AVector<Chunk> chunks_;
|
std::size_t block_size_{};
|
||||||
Chunk *last_alloc_chunk_{nullptr};
|
std::size_t data_size_;
|
||||||
Chunk *last_dealloc_chunk_{nullptr};
|
std::size_t alignment_;
|
||||||
|
|
||||||
|
AList<Chunk> chunks_; // TODO: do ourself so we can do fast Release (detect monotonic, do nothing)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static constexpr auto MaxBlocksInChunk() {
|
static constexpr auto MaxBlocksInChunk = std::numeric_limits<decltype(blocks_per_chunk_)>::max();
|
||||||
return std::numeric_limits<decltype(Chunk::first_available_block_ix)>::max();
|
|
||||||
}
|
|
||||||
|
|
||||||
Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *memory);
|
Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *chunk_memory);
|
||||||
|
|
||||||
Pool(const Pool &) = delete;
|
Pool(const Pool &) = delete;
|
||||||
Pool &operator=(const Pool &) = delete;
|
Pool &operator=(const Pool &) = delete;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2023 Memgraph Ltd.
|
// Copyright 2024 Memgraph Ltd.
|
||||||
//
|
//
|
||||||
// Use of this software is governed by the Business Source License
|
// Use of this software is governed by the Business Source License
|
||||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
@ -291,7 +291,7 @@ TEST(PoolResource, AllocationWithOverflow) {
|
|||||||
EXPECT_THROW(mem.Allocate((std::numeric_limits<size_t>::max() - 1U) / max_blocks_per_chunk, 1U), std::bad_alloc);
|
EXPECT_THROW(mem.Allocate((std::numeric_limits<size_t>::max() - 1U) / max_blocks_per_chunk, 1U), std::bad_alloc);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const size_t max_blocks_per_chunk = memgraph::utils::impl::Pool::MaxBlocksInChunk();
|
const size_t max_blocks_per_chunk = memgraph::utils::impl::Pool::MaxBlocksInChunk;
|
||||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, std::numeric_limits<size_t>::max());
|
memgraph::utils::PoolResource mem(max_blocks_per_chunk, std::numeric_limits<size_t>::max());
|
||||||
EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
||||||
// Throws because initial chunk block is aligned to
|
// Throws because initial chunk block is aligned to
|
||||||
|
Loading…
Reference in New Issue
Block a user