I have a question about combination of monotonic buffer and unsynchronized memory pool that are introduced in C++17. There is the following piece of code (source C++ Weekly - Ep 248)
#include <spdlog/spdlog.h>
#include <array>
#include <cassert>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>
// Prints if new/delete gets used.
class print_alloc : public std::pmr::memory_resource {
public:
print_alloc(std::string name, std::pmr::memory_resource* upstream)
: m_name(std::move(name)), m_upstream(upstream) {
assert(upstream);
}
private:
std::string m_name;
std::pmr::memory_resource* m_upstream;
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
spdlog::trace("[{} (alloc)] Size: {} Alignment: {} ...", m_name, bytes,
alignment);
auto result = m_upstream->allocate(bytes, alignment);
spdlog::trace("[{} (alloc)] ... Address: {}", m_name, result);
return result;
}
std::string format_destroyed_bytes(std::byte* p, const std::size_t size) {
std::string result = "";
bool in_string = false;
auto format_char = [](bool& in_string, const char c, const char next) {
auto format_byte = [](const char byte) {
return fmt::format(" {:02x}", static_cast<unsigned char>(byte));
};
if (std::isprint(static_cast<int>(c))) {
if (!in_string) {
if (std::isprint(static_cast<int>(next))) {
in_string = true;
return fmt::format(" \"{}", c);
} else {
return format_byte(c);
}
} else {
return std::string(1, c);
}
} else {
if (in_string) {
in_string = false;
return '"' + format_byte(c);
}
return format_byte(c);
}
};
std::size_t pos = 0;
for (; pos < std::min(size - 1, static_cast<std::size_t>(32)); ++pos) {
result += format_char(in_string, static_cast<char>(p[pos]),
static_cast<char>(p[pos + 1]));
}
result += format_char(in_string, static_cast<char>(p[pos]), 0);
if (in_string) {
result += '"';
}
if (pos < (size - 1)) {
result += " <truncated...>";
}
return result;
}
void do_deallocate(void* p, std::size_t bytes,
std::size_t alignment) override {
spdlog::trace(
"[{} (dealloc)] Address: {} Dealloc Size: {} Alignment: {} Data: {}",
m_name, p, bytes, alignment,
format_destroyed_bytes(static_cast<std::byte*>(p), bytes));
m_upstream->deallocate(p, bytes, alignment);
}
bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
};
template <typename Container, typename... Values>
auto create_container(auto* resource, Values&&... values) {
Container result{resource};
result.reserve(sizeof...(values));
(result.emplace_back(std::forward<Values>(values)), ...);
return result;
};
int main() {
spdlog::set_level(spdlog::level::trace);
print_alloc default_alloc{"Rogue PMR Allocation!",
std::pmr::null_memory_resource()};
std::pmr::set_default_resource(&default_alloc);
print_alloc oom{"Out of Memory", std::pmr::null_memory_resource()};
std::array<std::uint8_t, 32768> buffer{};
std::pmr::monotonic_buffer_resource underlying_bytes(buffer.data(),
buffer.size(), &oom);
print_alloc monotonic{"Monotonic Array", &underlying_bytes};
std::pmr::unsynchronized_pool_resource unsync_pool(&monotonic);
print_alloc pool("Pool", &unsync_pool);
for (int i = 0; i < 10; ++i) {
spdlog::debug("Starting Loop Iteration");
auto vec = create_container<std::pmr::vector<std::pmr::string>>(
&pool, "Hello", "World", "Hello Long String", "Another Long String");
spdlog::trace("Emplacing Long String");
vec.emplace_back("a different long string");
spdlog::trace("Emplacing Long String");
vec.emplace_back("a different long string 1");
spdlog::trace("Emplacing Long String");
vec.emplace_back("a different long string");
spdlog::trace("Emplacing Long String");
vec.emplace_back("a different long string");
spdlog::trace("Emplacing Short String");
vec.emplace_back("bob");
spdlog::trace("Emplacing Short String");
vec.emplace_back("was");
spdlog::trace("Erasing First Element");
vec.erase(vec.begin());
spdlog::trace("Erasing First Element");
vec.erase(vec.begin());
spdlog::trace("Erasing First Element");
vec.erase(vec.begin());
spdlog::debug("Finishing Loop Iteration");
// vec.push_back("Hello Long World");
}
spdlog::debug("Exiting Main");
}
Base on implementation of monotonic buffer, during deallocation do nothing. The buffer grows up only.
Checking the print out if this program, in every iteration the same regions of memory are used . This does not comply with the monotonic buffer implementation. What am I missing. In each iteration the allocate chunks which are retrieved form monotonic buffer are reused
What you're saying is expected as a result of the combination of
unsynchronized_pool_resourceandmonotonic_buffer_resources. Your saying thatmonotonic_buffer_resourcesshould allocate new memory and never reuse previously allocated memory - it does! The behaviour you're witnessing is as a result of theunsynchorinized_pool_resource- which is a resource that is designed to pool and reuse memory from it's underlying resource (in this case,monotonic_buffer_resources). When memory is deallocated, the pool resource retains that memory for future allocations rather than immediately returning it back to its resource.monotonic_buffer_resourceis still behaving monotonically - it is the pooling behaviour ofunsynchronized_pool_resourcethat gives the appearance of the memory reuse across those iterations.Combining these two allows for very efficient memory usage. Your program gets the benefit of rapid (and predictable) memory allocations from the
monotonic_buffer_resourceand the efficiency of memory reuse fromunsynchronized_pool_resource.Hope this cleared this up for you.
Demonstration
Here's a simple example of the utilisation of both:
When ran, an output like this is seen:
Here we can see how it would look like if we only used
monotonic_buffer_resource:And the output is what you would assume, you see a different memor address in each iteration - because
monotonic_buffer_resourceallocates new memory chunks from its buffer and never reuses previously allocated memory.