Commit d1573452 authored by Dominik Charousset's avatar Dominik Charousset

Implement monotonic_buffer_resource

parent 5820a182
...@@ -113,6 +113,7 @@ caf_add_component( ...@@ -113,6 +113,7 @@ caf_add_component(
src/detail/message_builder_element.cpp src/detail/message_builder_element.cpp
src/detail/message_data.cpp src/detail/message_data.cpp
src/detail/meta_object.cpp src/detail/meta_object.cpp
src/detail/monotonic_buffer_resource.cpp
src/detail/parse.cpp src/detail/parse.cpp
src/detail/parser/chars.cpp src/detail/parser/chars.cpp
src/detail/pretty_type_name.cpp src/detail/pretty_type_name.cpp
...@@ -250,6 +251,7 @@ caf_add_component( ...@@ -250,6 +251,7 @@ caf_add_component(
detail.limited_vector detail.limited_vector
detail.local_group_module detail.local_group_module
detail.meta_object detail.meta_object
detail.monotonic_buffer_resource
detail.parse detail.parse
detail.parser.read_bool detail.parser.read_bool
detail.parser.read_config detail.parser.read_config
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include <cstddef>
#include <map>
#include "caf/config.hpp"
#include "caf/detail/core_export.hpp"
#ifdef CAF_CLANG
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wc99-extensions"
#elif defined(CAF_GCC)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#elif defined(CAF_MSVC)
# pragma warning(push)
# pragma warning(disable : 4200)
#endif
namespace caf::detail {
/// Replacement for `std::pmr::monotonic_buffer_resource`, which sadly is not
/// available on all platforms CAF currenlty supports. This resource does not
/// support upstream resources and instead always uses `malloc` and `free`.
class CAF_CORE_EXPORT monotonic_buffer_resource {
public:
// -- member types -----------------------------------------------------------
/// A single block of memory.
struct block {
block* next;
std::byte bytes[];
};
/// A bucket for storing multiple blocks.
struct bucket {
block* head = nullptr;
std::byte* curr_pos = nullptr;
std::byte* curr_end = nullptr;
block* spare = nullptr;
size_t block_size = 0;
};
template <class T>
class allocator {
public:
using value_type = T;
template <class U>
struct rebind {
using other = allocator<U>;
};
explicit allocator(monotonic_buffer_resource* mbr) : mbr_(mbr) {
// nop
}
allocator() : mbr_(nullptr) {
// nop
}
allocator(const allocator&) = default;
allocator& operator=(const allocator&) = default;
template <class U>
allocator(const allocator<U>& other) : mbr_(other.resource()) {
// nop
}
template <class U>
allocator& operator=(const allocator<U>& other) {
mbr_ = other.resource();
return *this;
}
T* allocate(size_t n) {
return static_cast<T*>(mbr_->allocate(sizeof(T) * n, alignof(T)));
}
constexpr void deallocate(void*, size_t) noexcept {
// nop
}
constexpr auto resource() const noexcept {
return mbr_;
}
private:
monotonic_buffer_resource* mbr_;
};
// -- constructors, destructors, and assignment operators --------------------
monotonic_buffer_resource();
~monotonic_buffer_resource();
// -- allocator interface ----------------------------------------------------
/// Release all allocated memory to the OS even if no destructors were called
/// for the allocated objects.
void release();
/// Reclaims all allocated memory (re-using it) even if no destructors were
/// called for the allocated objects.
void reclaim();
/// Allocates memory.
[[nodiscard]] void* allocate(size_t bytes,
size_t alignment = alignof(max_align_t));
/// Fancy no-op.
constexpr void deallocate(void*, size_t, size_t = alignof(max_align_t)) {
// nop
}
/// Counts how many blocks currently exist in the bucket for @p alloc_size.
size_t blocks(size_t alloc_size);
/// Counts how many blocks currently exist in total.
size_t blocks();
private:
// Counts how many blocks exist in `where`.
size_t blocks(bucket& where);
// Gets the next free memory chunk.
[[nodiscard]] void* do_alloc(bucket& from, size_t bytes, size_t alignment);
// Adds another block to the bucket.
void grow(bucket& what);
// Select a bucket based on the allocation size.
bucket& bucket_by_size(size_t alloc_size);
// Sets all pointers back to nullptr.
void reset(bucket& bkt);
// Releases all memory in the bucket, leaving it in an invalid state.
void release(bucket& bkt);
// Shifts all blocks to `spare` list.
void reclaim(bucket& bkt);
// Objects of size <= 64 bytes.
bucket small_;
// Objects of size <= 512 bytes.
bucket medium_;
// Objects of various sizes > 512 bytes.
std::map<size_t, bucket> var_;
};
} // namespace caf::detail
#ifdef CAF_CLANG
# pragma clang diagnostic pop
#elif defined(CAF_GCC)
# pragma GCC diagnostic pop
#elif defined(CAF_MSVC)
# pragma warning(pop)
#endif
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#include "caf/detail/monotonic_buffer_resource.hpp"
#include <limits>
#include <memory>
#include "caf/raise_error.hpp"
namespace caf::detail {
monotonic_buffer_resource::monotonic_buffer_resource() {
// 8kb blocks for the small and medium sized buckets.
small_.block_size = 8 * 1024;
medium_.block_size = 8 * 1024;
}
monotonic_buffer_resource::~monotonic_buffer_resource() {
release(small_);
release(medium_);
for (auto& kvp : var_)
release(kvp.second);
}
void monotonic_buffer_resource::release() {
release(small_);
reset(small_);
release(medium_);
reset(medium_);
for (auto& kvp : var_) {
release(kvp.second);
reset(kvp.second);
}
}
void monotonic_buffer_resource::reclaim() {
reclaim(small_);
reclaim(medium_);
for (auto& kvp : var_)
reclaim(kvp.second);
}
void* monotonic_buffer_resource::allocate(size_t bytes, size_t alignment) {
return do_alloc(bucket_by_size(bytes), bytes, alignment);
}
size_t monotonic_buffer_resource::blocks(size_t alloc_size) {
return blocks(bucket_by_size(alloc_size));
}
size_t monotonic_buffer_resource::blocks() {
auto result = blocks(small_) + blocks(medium_);
for (auto& kvp : var_)
result += blocks(kvp.second);
return result;
}
size_t monotonic_buffer_resource::blocks(bucket& where) {
size_t result = 0;
for (auto ptr = where.head; ptr != nullptr; ptr = ptr->next)
++result;
for (auto ptr = where.spare; ptr != nullptr; ptr = ptr->next)
++result;
return result;
}
void* monotonic_buffer_resource::do_alloc(bucket& from, size_t bytes,
size_t alignment) {
for (;;) {
if (from.curr_pos != nullptr) {
auto result = static_cast<void*>(from.curr_pos);
auto space = static_cast<size_t>(from.curr_end - from.curr_pos);
if (std::align(alignment, bytes, result, space)) {
from.curr_pos = static_cast<std::byte*>(result) + bytes;
return result;
}
}
// Try again after allocating more storage.
grow(from);
}
}
void monotonic_buffer_resource::grow(bucket& what) {
auto init = [&what](block* blk) {
blk->next = what.head;
what.head = blk;
what.curr_pos = blk->bytes;
what.curr_end = reinterpret_cast<std::byte*>(blk) + what.block_size;
};
if (what.spare != nullptr) {
auto blk = what.spare;
what.spare = blk->next;
init(blk);
} else if (auto ptr = malloc(what.block_size)) {
init(static_cast<block*>(ptr));
} else {
CAF_RAISE_ERROR(std::bad_alloc, "monotonic_buffer_resource");
}
}
monotonic_buffer_resource::bucket&
monotonic_buffer_resource::bucket_by_size(size_t alloc_size) {
constexpr auto max_alloc_size = std::numeric_limits<size_t>::max()
- sizeof(block) - alignof(max_align_t);
auto var_bucket = [this](size_t key, size_t block_size) -> bucket& {
if (auto i = var_.find(key); i != var_.end()) {
return i->second;
} else {
bucket tmp;
tmp.block_size = block_size;
return var_.emplace(key, tmp).first->second;
}
};
if (alloc_size <= 64) {
return small_;
} else if (alloc_size <= 512) {
return medium_;
} else if (alloc_size <= 1'048'576) { // 1MB
// Align on 1kb and reserve memory for up to four elements.
auto bucket_key = ((alloc_size / 1024) + 1) * 1024;
return var_bucket(bucket_key, bucket_key * 4);
} else if (alloc_size <= max_alloc_size) {
// Fall back to individual allocations.
return var_bucket(alloc_size,
alloc_size + sizeof(block) + alignof(max_align_t));
} else {
CAF_RAISE_ERROR(std::bad_alloc, "monotonic_buffer_resource");
}
}
void monotonic_buffer_resource::reset(bucket& bkt) {
bkt.head = nullptr;
bkt.curr_pos = nullptr;
bkt.curr_end = nullptr;
bkt.spare = nullptr;
}
void monotonic_buffer_resource::release(bucket& bkt) {
for (auto ptr = bkt.head; ptr != nullptr;) {
auto blk = ptr;
ptr = ptr->next;
free(blk);
}
for (auto ptr = bkt.spare; ptr != nullptr;) {
auto blk = ptr;
ptr = ptr->next;
free(blk);
}
}
void monotonic_buffer_resource::reclaim(bucket& bkt) {
for (auto ptr = bkt.head; ptr != nullptr;) {
auto blk = ptr;
ptr = ptr->next;
blk->next = bkt.spare;
bkt.spare = blk;
}
bkt.head = nullptr;
bkt.curr_pos = nullptr;
bkt.curr_end = nullptr;
}
} // namespace caf::detail
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE detail.monotonic_buffer_resource
#include "caf/detail/monotonic_buffer_resource.hpp"
#include "core-test.hpp"
#include <list>
#include <map>
#include <vector>
using namespace caf;
SCENARIO("monotonic buffers group allocations") {
GIVEN("a monotonic buffer resource") {
detail::monotonic_buffer_resource mbr;
WHEN("calling allocate multiple times for the same size") {
THEN("the resource returns consecutive pointers") {
CHECK_EQ(mbr.blocks(8), 0u);
auto p1 = mbr.allocate(8);
auto p2 = mbr.allocate(8);
auto p3 = mbr.allocate(8);
CHECK_EQ(mbr.blocks(8), 1u);
CHECK(p1 < p2);
CHECK(p2 < p3);
}
}
}
GIVEN("a monotonic buffer resource") {
detail::monotonic_buffer_resource mbr;
WHEN("calling allocate with various sizes") {
THEN("the resource puts allocations into buckets") {
void* unused = nullptr; // For silencing nodiscard warnings.
CHECK_EQ(mbr.blocks(), 0u);
MESSAGE("perform small allocations");
unused = mbr.allocate(64);
CHECK_EQ(mbr.blocks(), 1u);
unused = mbr.allocate(64);
CHECK_EQ(mbr.blocks(), 1u);
MESSAGE("perform medium allocations");
unused = mbr.allocate(65);
CHECK_EQ(mbr.blocks(), 2u);
unused = mbr.allocate(512);
CHECK_EQ(mbr.blocks(), 2u);
MESSAGE("perform large allocations <= 1 MB (pools allocations)");
unused = mbr.allocate(513);
CHECK_EQ(mbr.blocks(), 3u);
unused = mbr.allocate(1023);
CHECK_EQ(mbr.blocks(), 3u);
MESSAGE("perform large allocations > 1 MB (allocates individually)");
unused = mbr.allocate(1'048'577);
CHECK_EQ(mbr.blocks(), 4u);
unused = mbr.allocate(1'048'577);
CHECK_EQ(mbr.blocks(), 5u);
}
}
}
}
SCENARIO("monotonic buffers re-use memory after calling reclaim") {
std::vector<void*> locations;
GIVEN("a monotonic buffer resource with some allocations performed on it") {
detail::monotonic_buffer_resource mbr;
locations.push_back(mbr.allocate(64));
locations.push_back(mbr.allocate(64));
locations.push_back(mbr.allocate(65));
locations.push_back(mbr.allocate(512));
locations.push_back(mbr.allocate(513));
locations.push_back(mbr.allocate(1023));
locations.push_back(mbr.allocate(1'048'577));
locations.push_back(mbr.allocate(1'048'577));
WHEN("calling reclaim on the resource") {
mbr.reclaim();
THEN("performing the same allocations returns the same addresses again") {
if (CHECK_EQ(mbr.blocks(), 5u)) {
CHECK_EQ(locations[0], mbr.allocate(64));
CHECK_EQ(locations[1], mbr.allocate(64));
CHECK_EQ(locations[2], mbr.allocate(65));
CHECK_EQ(locations[3], mbr.allocate(512));
CHECK_EQ(locations[4], mbr.allocate(513));
CHECK_EQ(locations[5], mbr.allocate(1023));
CHECK_EQ(locations[6], mbr.allocate(1'048'577));
CHECK_EQ(locations[7], mbr.allocate(1'048'577));
CHECK_EQ(mbr.blocks(), 5u);
}
}
}
}
}
SCENARIO("monotonic buffers provide storage for STL containers") {
GIVEN("a monotonic buffer resource and a std::vector") {
using int_allocator = detail::monotonic_buffer_resource::allocator<int32_t>;
detail::monotonic_buffer_resource mbr;
WHEN("pushing to the vector") {
THEN("the memory resource fills up") {
CHECK_EQ(mbr.blocks(), 0u);
std::vector<int32_t, int_allocator> xs{int_allocator{&mbr}};
xs.push_back(42);
CHECK_EQ(xs.size(), 1u);
CHECK_EQ(mbr.blocks(), 1u);
xs.insert(xs.end(), 17, 0);
}
}
}
GIVEN("a monotonic buffer resource and a std::list") {
using int_allocator = detail::monotonic_buffer_resource::allocator<int32_t>;
detail::monotonic_buffer_resource mbr;
WHEN("pushing to the list") {
THEN("the memory resource fills up") {
CHECK_EQ(mbr.blocks(), 0u);
std::list<int32_t, int_allocator> xs{int_allocator{&mbr}};
xs.push_back(42);
CHECK_EQ(xs.size(), 1u);
CHECK_EQ(mbr.blocks(), 1u);
}
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment