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
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_resource
and monotonic_buffer_resources
. Your saying that monotonic_buffer_resources
should allocate new memory and never reuse previously allocated memory - it does! The behaviour you're witnessing is as a result of the unsynchorinized_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_resource
is still behaving monotonically - it is the pooling behaviour of unsynchronized_pool_resource
that 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_resource
and the efficiency of memory reuse from unsynchronized_pool_resource
.
Hope this cleared this up for you.
Here's a simple example of the utilisation of both:
#include <iostream>
#include <memory_resource>
int main() {
char buffer[1024];
std::pmr::monotonic_buffer_resource monotonicResource(buffer, sizeof(buffer));
// Use an unsynchronized_pool_resource on top of the monotonic_buffer_resource
std::pmr::unsynchronized_pool_resource poolResource(&monotonicResource);
// Allocate and deallocate memory using the unsynchronized_pool_resource
for (int i = 0; i < 5; ++i) {
std::cout << "Iteration " << i << ":\n";
// Allocate memory
void* ptr = poolResource.allocate(200); // request 200 bytes from the pool
std::cout << "Allocated at: " << ptr << "\n";
// Deallocate memory (returning it to the pool! Not the monotonic resource)
poolResource.deallocate(ptr, 200);
std::cout << "Deallocated.\n\n";
}
return 0;
}
When ran, an output like this is seen:
Iteration 0:
Allocated at: 00000244C378AB00
Deallocated
Iteration 1:
Allocated at: 00000244C378AB00
Deallocated
Iteration 2:
Allocated at: 00000244C378AB00
Deallocated
Iteration 3:
Allocated at: 00000244C378AB00
Deallocated
Iteration 4:
Allocated at: 00000244C378AB00
Deallocated
Here we can see how it would look like if we only used monotonic_buffer_resource
:
#include <iostream>
#include <memory_resource>
int main() {
char buffer[1024];
std::pmr::monotonic_buffer_resource monotonicResource(buffer, sizeof(buffer));
// Allocate memory directly from the monotonic_buffer_resource
for (int i = 0; i < 5; ++i) {
std::cout << "Iteration " << i << ":\n";
// Allocate memory directly from the motonic resource
void* ptr = monotonicResource.allocate(200); // request 200 bytes
std::cout << "Allocated at: " << ptr << "\n\n";
}
return 0;
}
And the output is what you would assume, you see a different memor address in each iteration - because monotonic_buffer_resource
allocates new memory chunks from its buffer and never reuses previously allocated memory.
Iteration 0:
Allocated at: 000000394CEFF270
Iteration 1:
Allocated at: 000000394CEFF338
Iteration 2:
Allocated at: 000000394CEFF400
Iteration 3:
Allocated at: 000000394CEFF4C8
Iteration 4:
Allocated at: 000000394CEFF590