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 {
|
||||
|
||||
Pool::Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *memory)
|
||||
: blocks_per_chunk_(blocks_per_chunk), block_size_(block_size), chunks_(memory) {}
|
||||
|
||||
Pool::~Pool() { MG_ASSERT(chunks_.empty(), "You need to call Release before destruction!"); }
|
||||
|
||||
void *Pool::Allocate() {
|
||||
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_;
|
||||
Pool::Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *chunk_memory)
|
||||
: blocks_per_chunk_(blocks_per_chunk),
|
||||
block_size_(block_size),
|
||||
data_size_{blocks_per_chunk_ * block_size_},
|
||||
alignment_{Ceil2(block_size_)},
|
||||
chunks_(chunk_memory) {
|
||||
// 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
|
||||
// that requested alignment of particular blocks is never greater than the
|
||||
// block itself.
|
||||
size_t alignment = Ceil2(block_size_);
|
||||
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;
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
last_alloc_chunk_ = &*it;
|
||||
last_dealloc_chunk_ = &*it;
|
||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
||||
Pool::~Pool() {
|
||||
if (!chunks_.empty()) Release();
|
||||
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) {
|
||||
MG_ASSERT(last_dealloc_chunk_, "No chunk to deallocate");
|
||||
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
|
||||
*reinterpret_cast<std::byte **>(p) = std::exchange(free_list_, reinterpret_cast<std::byte *>(p));
|
||||
}
|
||||
|
||||
void Pool::Release() {
|
||||
for (auto &chunk : chunks_) {
|
||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
||||
size_t alignment = Ceil2(block_size_);
|
||||
GetUpstreamResource()->Deallocate(chunk.data, data_size, alignment);
|
||||
auto *resource = GetUpstreamResource();
|
||||
if (!dynamic_cast<utils::MonotonicBufferResource *>(resource)) {
|
||||
for (auto &chunk : chunks_) {
|
||||
resource->Deallocate(chunk.raw_data, data_size_, alignment_);
|
||||
}
|
||||
}
|
||||
chunks_.clear();
|
||||
last_alloc_chunk_ = nullptr;
|
||||
last_dealloc_chunk_ = nullptr;
|
||||
free_list_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace impl
|
||||
@ -262,7 +205,7 @@ PoolResource::PoolResource(size_t max_blocks_per_chunk, size_t max_block_size, M
|
||||
MemoryResource *memory_unpooled)
|
||||
: pools_(memory_pools),
|
||||
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) {
|
||||
MG_ASSERT(max_blocks_per_chunk_ > 0U, "Invalid number of blocks per chunk");
|
||||
MG_ASSERT(max_block_size_ > 0U, "Invalid size of block");
|
||||
|
@ -16,6 +16,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <forward_list>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
@ -382,37 +385,47 @@ class MonotonicBufferResource final : public MemoryResource {
|
||||
|
||||
namespace impl {
|
||||
|
||||
template <class T>
|
||||
using AList = std::forward_list<T, Allocator<T>>;
|
||||
|
||||
template <class T>
|
||||
using AVector = std::vector<T, Allocator<T>>;
|
||||
|
||||
/// 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
|
||||
/// taken from `libstdc++` implementation, but the implementation details are
|
||||
/// more similar to `FixedAllocator` described in "Small Object Allocation" from
|
||||
/// "Modern C++ Design".
|
||||
/// Chunk runs out of available blocks, a new Chunk is allocated.
|
||||
class Pool final {
|
||||
/// Holds a pointer into a chunk of memory which consists of equal sized
|
||||
/// blocks. Each Chunk can handle `std::numeric_limits<unsigned char>::max()`
|
||||
/// number of blocks. Blocks form a "free-list", where each unused block has
|
||||
/// an embedded index to the next unused block.
|
||||
/// blocks. Blocks form a "free-list"
|
||||
struct Chunk {
|
||||
unsigned char *data;
|
||||
unsigned char first_available_block_ix;
|
||||
unsigned char blocks_available;
|
||||
// TODO: make blocks_per_chunk a per chunk thing (ie. allow chunk growth)
|
||||
std::byte *raw_data;
|
||||
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_;
|
||||
size_t block_size_;
|
||||
AVector<Chunk> chunks_;
|
||||
Chunk *last_alloc_chunk_{nullptr};
|
||||
Chunk *last_dealloc_chunk_{nullptr};
|
||||
std::byte *free_list_{nullptr};
|
||||
uint8_t blocks_per_chunk_{};
|
||||
std::size_t block_size_{};
|
||||
std::size_t data_size_;
|
||||
std::size_t alignment_;
|
||||
|
||||
AList<Chunk> chunks_; // TODO: do ourself so we can do fast Release (detect monotonic, do nothing)
|
||||
|
||||
public:
|
||||
static constexpr auto MaxBlocksInChunk() {
|
||||
return std::numeric_limits<decltype(Chunk::first_available_block_ix)>::max();
|
||||
}
|
||||
static constexpr auto MaxBlocksInChunk = std::numeric_limits<decltype(blocks_per_chunk_)>::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 &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
|
||||
// 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);
|
||||
}
|
||||
{
|
||||
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());
|
||||
EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
||||
// Throws because initial chunk block is aligned to
|
||||
|
Loading…
Reference in New Issue
Block a user