Polymorphic Allocators from C++17, std:vector Growth and Hacking

栏目: IT技术 · 发布时间: 4年前

内容简介:The concept of a polymorphic allocator from C++17 is an enhancement to standard allocators from the Standard Library.It’s much easier to use than a regular allocator and allows containers to have the same type while having a different allocator, or even a

Polymorphic Allocators from C++17, std:vector Growth and Hacking

The concept of a polymorphic allocator from C++17 is an enhancement to standard allocators from the Standard Library.

It’s much easier to use than a regular allocator and allows containers to have the same type while having a different allocator, or even a possibility to change allocators at runtime.

Let’s see how we can use it and hack to see the growth of std::vector containers.

  • Core elements of pmr:
    • new_delete_resource()
    • null_memory_resource()
    • synchronized_pool_resource
    • unsynchronized_pool_resource
    • monotonic_buffer_resource
    • A Much Better Solution

In short, a polymorphic allocator conforms to the rules of an allocator from the Standard Library. Still, at its core, it uses a memory resource object to perform memory management.

Polymorphic Allocator contains a pointer to a memory resource class, and that’s why it can use a virtual method dispatch. You can change the memory resource at runtime while keeping the type of the allocator. This is the opposite to regular allocators which make two containers using a different allocator also a different type.

All the types for polymorphic allocators live in a separate namespace std::pmr (PMR stands for Polymorphic Memory Resource), in the <memory_resource> header.

The Series

This article is part of my series about C++17 Library Utilities. Here’s the list of the articles:

Polymorphic Allocators from C++17, std:vector Growth and Hacking

Resources about C++17 STL:

OK, let’s go back to our main topic: PMR.

Core elements of pmr :

Here’s a little summary of the main parts of pmr :

  • std::pmr::memory_resource - is an abstract base class for all other implementations. It defines the following pure virtual methods:
    virtual void* do_allocate(std::size_t bytes, std::size_t alignment)
    virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment)
    virtual bool do_is_equal(const std::pmr::memory_resource& other) const noexcept
    
  • std::pmr::polymorphic_allocator - is an implementation of a standard allocator that uses memory_resource object to perform memory allocations and deallocations.
  • global memory resources accessed by new_delete_resource() and null_memory_resource()
  • a set of predefined memory pool resource classes:
    synchronized_pool_resource
    unsynchronized_pool_resource
    monotonic_buffer_resource
    
  • template specialisations of the standard containers with polymorphic allocator, for example std::pmr::vector , std::pmr::string , std::pmr::map and others. Each specialisation is defined in the same header file as the corresponding container.
  • It’s also worth mentioning that pool resources (including monotonic_buffer_resource ) can be chained. If there’s no available memory in a pool, the allocator will allocate from the “upstream” resource.

And we have the following predefined memory resources:

new_delete_resource()

It’s a free function that returns a pointer to a global “default” memory resource. It manages memory with the global new and delete .

null_memory_resource()

It’s a free function that returns a pointer to a global “null” memory resource which throws std::bad_alloc on every allocation. While it sounds not useful, it might be handy when you want to guarantee that your objects don’t allocate any memory on the heap. Or for testing.

synchronized_pool_resource

This is a thread-safe allocator that manages pools of different sizes. Each pool is a set of chunks that are divided into blocks of uniform size.

unsynchronized_pool_resource

A non-thread-safe pool_resource .

monotonic_buffer_resource

This is a non-thread-safe, fast, special-purpose resource that gets memory from a preallocated buffer, but doesn’t release it with deallocation. It can only grow.

An Example

Below you can find a simple example of monotonic_buffer_resource and pmr::vector :

#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>            // pmr::vector

int main() {
    char buffer[64] = {}; // a small buffer on the stack
    std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
    std::cout << buffer << '\n';

    std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};

    std::pmr::vector<char> vec{ &pool };
    for (char ch = 'a'; ch <= 'z'; ++ch)
        vec.push_back(ch);

    std::cout << buffer << '\n';
}

Possible output:

_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______

In the above example, we use a monotonic buffer resource initialised with a memory chunk from the stack. By using a simple char buffer[] array, we can easily print the contents of the “memory”. The vector gets memory from the pool (and it’s super fast since it’s on the stack), and if there’s no more space available, it will ask for memory from the “upstream” resource. The example shows vector reallocations when there’s a need to insert more elements. Each time the vector gets more space, so it eventually fits all of the letters. The monotonic buffer resource doesn’t delete any memory as you can see, it only grows.

We could also use reserve() on the vector, and that would limit the number of memory allocations, but the point of this example was to illustrate the "expansion" of the container.

I mentioned that if the memory ends then the allocator will get memory from the upstream resource. How can we observe it?

Some Hacks

At start let’s try and do some hacking :)

In our case, the upstream memory resource is a default one as we didn’t change it. That means new() and delete() . However, we have to keep in mind that do_allocate() and do_deallocate() member functions also take an alignment parameter.

That’s why if we want to hack and see if the memory is allocated by new() we have to use C++17’s new() with the alignment support:

void* lastAllocatedPtr = nullptr;
size_t lastSize = 0;

void* operator new(std::size_t size, std::align_val_t align) {
#if defined(_WIN32) || defined(__CYGWIN__)
    auto ptr = _aligned_malloc(size, static_cast<std::size_t>(align));
#else
    auto ptr = aligned_alloc(static_cast<std::size_t>(align), size);
#endif

    if (!ptr)
        throw std::bad_alloc{};

    std::cout << "new: " << size << ", align: " 
              << static_cast<std::size_t>(align) 
              << ", ptr: " << ptr << '\n';

    lastAllocatedPtr = ptr;
    lastSize = size;

    return ptr;
}

In the above code part I implemented aligned new() (you can read more about this whole new feature in my separate article: New new() - The C++17’s Alignment Parameter for Operator new() ).

And you can also spot two ugly global variables :) However, thanks to them we can see when our memory goes:

Let’s reconsider our example:

constexpr auto buf_size = 32;
uint16_t buffer[buf_size] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, 0);

std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)*sizeof(uint16_t)};

std::pmr::vector<uint16_t> vec{ &pool };

for (int i = 1; i <= 20; ++i)
    vec.push_back(i);

for (int i = 0; i < buf_size; ++i)
    std::cout <<  buffer[i] << " ";

std::cout << std::endl;

auto* bufTemp = (uint16_t *)lastAllocatedPtr;

for (unsigned i = 0; i < lastAllocatedSize; ++i)
    std::cout << bufTemp[i] << " ";

This time we store uint16_t rather than char .

The program tries to store 20 numbers in a vector, but since the vector grows, then we need more than the predefined buffer (only 32 entries). That’s why at some point the allocator turns to global new and delete.

Here’s a possible output that you might get:

new: 128, align: 16, ptr: 0x21b3c20
1 1 2 1 2 3 4 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 0 0 0 0 0 .....
delete: 128, align: 16, ptr : 0x21b3c20

It looks like the predefined buffer could store only up to 16th elements, but when we inserted number 17, then the vector had to grow, and that’s why we see the new allocation - 128 bytes.

The second line shows the contents of the custom buffer, while the third line shows the memory allocated through new() .

Here’s a live version @Coliru

A Much Better Solution

The previous example worked and shows us something, but hacking with new() and delete() is not what you should do in production code. In fact, memory resources are extensible, and if you want the best solution, you can roll your resource!

All you have to do is to implement the following:

  • Derive from std::pmr::memory_resource
  • Implement:
    do_allocate()
    do_deallocate()
    do_is_equal()
    
  • Set your custom memory resource as active for your objects and containers.

And here are the resources that you can see to learn how to implement it.

Summary

Through this article, I wanted to shows you some basic examples with pmr and the concept of a polymorphic allocator. As you can see, setting up an allocator for a vector is much simpler than it was with regular allocators. There is a set of predefined allocators at your disposal, and it’s relatively easy to implement your custom version. The code in the article showed just a simple hacking to illustrate where the memory is pulled from.

Back to you:

Do you use custom memory allocators?

Have you played with pmr and polymorphic allocators from C++?

Let us know in comments.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构教程

数据结构教程

彭波 / 第1版 (2004年3月1日) / 2004-3-1 / 34.00元

精心策划,准确定位 概念清晰,例题丰富 深入浅出,内容翔实 体系合理,重点突出一起来看看 《数据结构教程》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试