C ++ 17多态分配器

很快,课程“ C ++开发人员”的新内容将不断涌现。专业“在课程开始前夕,我们的专家Alexander Klyuchev准备了有关多态分配器的有趣材料。我们请亚历山大发言:










在本文中,我想展示使用pmr名称空间中的组件的简单示例以及多态分配器基础的基本思想。



c ++ 17中引入的多态分配器的主要思想是改进基于静态多态性或换句话说模板实现的标准分配器。它们比标准分配器更易于使用,此外,它们允许您在使用不同的分配器时维护容器的类型,因此可以在运行时更改分配器。



如果要std::vector使用特定的内存分配器,则可以使用Allocator模板参数:



auto my_vector = std::vector<int, my_allocator>();




但是有一个问题-此向量与具有不同分配器的向量的类型不同,包括默认情况下定义的分配器。

这样的容器不能传递给需要带有默认容器的向量的函数,也不能将具有不同分配器类型的两个向量分配给同一变量,例如:



auto my_vector = std::vector<int, my_allocator>();
auto my_vector2 = std::vector<int, other_allocator>();
auto vec = my_vector; // ok
vec = my_vector2; // error


多态分配器包含指向接口的指针,memory_resource以便它可以使用动态分配。



要更改使用内存的策略,只需替换实例memory_resource,并保留分配器的类型即可。这也可以在运行时完成。否则,多态分配器将按照与标准分配器相同的规则工作。



新分配器使用的特定数据类型在名称空间中std::pmr也有可以与多态分配器一起使用的标准容器的模板专业化。



目前的主要问题之一是的新版本容器std::pmr与的类似物不兼容std



主要成分 std::pmr:



  • std::pmr::memory_resource — , .
  • :

    • 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 — , memory_resource .
  • new_delete_resource() null_memory_resource() «»
  • :

    • synchronized_pool_resource
    • unsynchronized_pool_resource
    • monotonic_buffer_resource
  • , std::pmr::vector, std::pmr::string, std::pmr::map . , .
  • memory_resource:

    • memory_resource* new_delete_resource() , memory_resource, new delete .
    • memory_resource* null_memory_resource()

      free函数返回一个指针,每次分配尝试时都会memory_resource向该指针抛出异常std::bad_alloc

      这对于确保对象不会在堆上分配内存或用于测试目的很有用。




  • class synchronized_pool_resource : public std::pmr::memory_resource

    线程安全的通用memory_resource实现由一组具有不同大小的内存块的池组成。

    每个池都是相同大小的内存块的集合。
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    单线程版本synchronized_pool_resource
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    单线程,快速, memory_resource专用从预先分配的缓冲区中获取内存,但不会释放它,也就是说,它只能增长。


用法示例monotonic_buffer_resourcepmr::vector



#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>        	// pmr::vector
#include <string>        	// pmr::string
 
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';
}


程序输出:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


在上面的示例中,我们使用monotonic_buffer_resource,使用在堆栈上分配的缓冲区进行初始化。使用指向此缓冲区的指针,我们可以轻松显示内存的内容。



向量从池中获取内存,这是非常快的,因为它在堆栈上,如果它用完了内存,它会使用global运算符请求它new该示例演示了尝试插入多于保留数量的元素时的矢量实现。在这种情况下,monotonic_buffer旧的内存不会被释放,只会增长。



当然,您可以调用reserve()向量来最大程度地减少重新分配,但是该示例的目的恰恰是为了演示它如何monotonic_buffer_resource随着容器的扩展而变化



存储 pmr::string



如果我们要在其中存储字符串pmr::vector怎么办?



一个重要的功能是,如果容器中的对象也使用多态分配器,则它们会请求父容器的分配器进行内存管理。



如果你想利用这个功能的优势,你必须使用std::pmr::string替代std::string



考虑用缓冲堆栈,我们将通过作为预先分配的一个例子memory_resourcestd::pmr::vector std::pmr::string



#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>        	// pmr::vector
#include <string>        	// pmr::string
 
int main() {
	std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
	std::cout << "sizeof(std::pmr::string): " << sizeof(std::pmr::string) << '\n';
 
	char buffer[256] = {}; // a small buffer on the stack
	std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
 
	const auto BufferPrinter = [](std::string_view buf, std::string_view title) {
    	std::cout << title << ":\n";
    	for (auto& ch : buf) {
        	std::cout << (ch >= ' ' ? ch : '#');
    	}
    	std::cout << '\n';
	};
 
	BufferPrinter(buffer, "zeroed buffer");
 
	std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
	std::pmr::vector<std::pmr::string> vec{ &pool };
	vec.reserve(5);
 
	vec.push_back("Hello World");
	vec.push_back("One Two Three");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after two short strings");
 
	vec.emplace_back("This is a longer string");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after longer string strings");
 
	vec.push_back("Four Five Six");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after the last string");   
}


程序输出:



sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________#
after longer string strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________________________________________________________________________________________This is a longer string#_______________________________#
after the last string:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________#m### ###n### ##########Four Five Six###________________________________________This is a longer string#_______________________________#


此示例中要注意的要点:



  • 大小pmr::string大于std::string这是由于指向memory_resource
  • 我们为5个元素保留向量,因此添加4时不会发生重新分配。
  • 前两行对于矢量存储块来说足够短,因此不会发生其他存储分配。
  • 第三行较长,需要在缓冲区内有单独的内存块,并且仅指向该块的指针存储在向量中。
  • 从输出中可以看到,“这是一个更长的字符串”几乎位于缓冲区的最末端。
  • 当我们插入另一个短字符串时,它会退回到向量的存储块中


为了进行比较,让我们用std::string代替std::pmr::string



sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
###w# ##########Hello World########w# ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________________________#
new 24
after longer string strings:
###w# ##########Hello World########w# ##########One Two Three###0#######################_______________________________________________________________________________________________________________________________________________________________________#
after the last string:
###w# ##########Hello World########w# ##########One Two Three###0#######################________@##w# ##########Four Five Six###_______________________________________________________________________________________________________________________________#




这次,由于不需要存储指向memory_resource的指针,因此容器中的项目将占用较少的空间。

短字符串仍然存储在向量存储块中,但是现在长字符串没有进入我们的缓冲区。这次使用默认分配器分配了一个长字符串,并将

指向它指针放置在向量存储块中因此,我们在输出中看不到该行。



再次关于向量展开:



提到了当池中的内存用完时,分配器使用operator来请求它new()



其实,这并不完全正确-存储从请求memory_resource,使用免费的函数返回

std::pmr::memory_resource* get_default_resource()

默认情况下,这个函数返回

std::pmr::new_delete_resource(),这反过来分配内存使用的运营商new(),但可使用的功能来代替

std::pmr::memory_resource* set_default_resource(std::pmr::memory_resource* r)



那么,让我们来看一个例子,当它get_default_resource返回一个值通过默认。



应该牢记的是,这些方法do_allocate()do_deallocate()使用“ alignment”参数,因此我们需要new()具有对齐支持的C ++ 17版本



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;
}


现在让我们回到主要示例:



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 < lastSize; ++i)
	std::cout << bufTemp[i] << " ";


该程序尝试将20个数字放入向量中,但是鉴于向量仅在增长,因此我们需要的空间比带有32个条目的保留缓冲区中的空间更大。



因此,在某个时候,分配器将通过请求内存get_default_resource,这将导致对global的调用new()



程序输出:



new: 128, align: 16, ptr: 0xc73b20
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 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 132 0 0 0 0 0 0 0 144 0 0 0 65 0 0 0 16080 199 0 0 16176 199 0 0 16176 199 0 0 15344 199 0 0 15472 199 0 0 15472 199 0 0 0 0 0 0 145 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


从控制台的输出来看,分配的缓冲区仅够16个元素,当我们插入数字17时,使用operator会重新分配128个字节new()



在第三行,我们看到了一个使用operator分配的内存块new()



上面带有操作符替代的示例new()不太可能适用于产品解决方案。



幸运的是,没有人打扰我们自己实现接口memory_resource



我们需要的是



  • 继承自 std::pmr::memory_resource
  • 实现方法:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • 将我们的实现传递给memory_resource容器。


就这样。通过下面的链接,您可以观看开放日的记录,我们在其中详细讨论课程计划,学习过程并回答潜在学生的问题:





阅读更多






All Articles