C++17 多态内存管理 pmr
概念
C++17开始,增加特性 Polymorphic Memory Resources 多态内存资源,缩写 PMR。
提供新的内存分配策略,更灵活地控制内存的分配与回收——适用于嵌入式和高并发服务器场景。
-
对内存资源的抽象
抽象基类
std::pmr::memory_resource
定义了用于内存的分配和回收的接口 -
运行时灵活性
- 能够在运行时更改容器内存分配策略
- 允许选择 / 实现自定义的
std::pmr::memory_resource
内存资源管理类 - 可将memory_resource作为参数传递给基于polymorpical_allocator的特化容器使用
- 允许选择 / 实现自定义的
- 与标准的
std::allocator
不同,能够在不同类型的容器实例之间传递
- 能够在运行时更改容器内存分配策略
-
性能和功能上的改进
-
合适的内存分配策略,可以减少系统调用 —— 例如 内存池的思想,避免malloc每次分配时的系统调用。
-
存在方法以确保分配的内存连续,有利于使用到CPU缓存
-
重定向堆内存调用,从而复用栈上分配的内存
-
提供了实现其它功能的自由度(缓冲区管理、内存池、共享内存场景…)
例如:把容器和元素放在可以被多个进程访问的共享内存中
使用特殊内存区域(如 NUMA 节点)、跟踪和分析内存使用、提供预分配的内存池、实现垃圾回收机制、提供线程本地存储以优化并发
-
在没有pmr的时代,使用STL容器有时会出现内存分配效率低的问题。例如:std::list
每个节点都会指向下一个节点,每次推入新元素都需要malloc一次。
C++17 为了便捷的支持预定义的和用户定义的内存分配方式,引入了名称空间std::pmr
,其包含一系列使用PMR的容器。现在标准库的所有容器都可以设置分配的内存池,并且具有友好的语法糖。
例如
std::pmr::unordered_map
是std::unordered_map
的一个特化版本
使用了多态分配器 (
std::pmr::polymorphic_allocator
)可以使用外部提供的任意
std::pmr::memory_resource
实例。
pmr基本使用
不使用 pmr 容器
#include <iostream>
#include <string>
#include <vector>
#include "../lang/tracknew.hpp"
/* 不使用 pmr,需要 1018 次分配堆内存 */
// - vector扩容是非常耗资源的事情
// - 每一个没有使用短字符串优化的 string还要分配一次
// 共需要 1018 次分配堆内存
TrackNew::reset();
std::vector<std::string> coll;
for (int i = 0; i < 1000; ++i) {
coll.emplace_back("just a non‐SSO string");
}
TrackNew::status();
vector resize:为内存(重新)分配在一些环境中需要很长时间(例如嵌入式系统),完全在堆上分配内存可能会导致性能问题。
- 事先知道要存储的元素的数量 —— 最最理想
- 重新分配内存是不可避免的 —— 大多数情况
- 在避免重新分配和不想浪费太多内存之间权衡
使用pmr容器
当缓冲区没有足够的内存时还会在堆上分配新的内存。
非pmr元素
/* 使用 PMR - monotopic_buffer_resource */
// vector的 18 次内存分配将不会再发生在堆上,而是在初始化的 buf里发生
// 共需要 1000 次分配堆内存
TrackNew::reset();
// - 在栈上分配一些内存, 用作pmr容器的初始内存池
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
// - 使用 std::pmr 容器 std::pmr::vector
// 实际类型 std::vector<std::string, std::pmr::polymorphic_allocator <std::string>>
std::pmr::vector<std::string> coll{&pool};
for (int i = 0; i < 1000; ++i) {
coll.emplace_back("just a non‐SSO string");
}
TrackNew::status();
pmr元素
pmr vector 会尝试把它的分配器传播给它的元素
- 当元素使用多态分配器时(例如
std::pmr::string
类型),传播不会失败。 - 当元素并不使用多态分配器(std::string),SFINE 匹配失败,无事发生。
std::pmr::string
类型 和std::string
类型之间有显式的转换,但没有隐式的转换支持显式转换是因为所有的字符串都可以隐式转换为
std::string_view
字符串视图,std::string_view
又可以显式转换为任何字符串类型。
/* 使用 PMR - monotopic_buffer_resource */
TrackNew::reset();
// - 在栈上分配一些内存, 用作pmr容器的初始内存池
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
// 元素类型改为 std::pmr::string来避免任何内存分配
std::pmr::vector<std::pmr::string> coll{&pool};
for (int i = 0; i < 1000; ++i) {
coll.emplace_back("just a non‐SSO string");
}
TrackNew::status();
标准内存资源
内存资源都以指针的形式传递。必须保证指针指向的资源对象直到最后释放内存之前都一直有效。
如果到处 move对象并且内存资源可互换,可能导致最后释放内存的时机比预期的更晚。
总览
为了支持多态分配器,C++标准库提供了如下内存资源:
三个 pmr 分配器类
-
都是类,继承自抽象基类
std::pmr::memory_resource
-
必须 1.创建分配器对象,2.把分配器对象指针传递给目标pmr对象
memory_resource派生类 | 效率 | 线程安全性 | 内存 |
---|---|---|---|
monotopic_buffer_resource | 效率最高 | 线程不安全 | “只进不出”(从不释放、可传递进可选的缓冲区) |
unsynchronized_pool_resource | 效率较高(内部不需要上锁) | 线程不安全(各线程所私有) | 更少碎片化 |
synchronized_pool_resource | 效率低(内部需要上锁) | 线程安全(允许多线程共享) | 更少碎片化 |
两个返回指向单例全局内存资源的指针的函数
返回指向单例全局内存资源的指针 | 特点 |
---|---|
new_delete_resouece() 函数 | 默认的内存资源(转发给传统 new/delete) |
null_memory_resource() 函数 | “永远拒绝” |
/* 3个 memory_resource派生类 */
// 1. std::pmr::monotonic_buffer_resource
// - 创建不释放内存、按块分配内存、指定地址和大小 的内存池
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool1{buf.data(), buf.size()};
// - 创建不释放内存、按块分配内存、未指定初始大小 的内存池
std::pmr::monotonic_buffer_resource pool1;
// - 创建不释放内存、按块分配内存、初始大小为10k 的内存池
std::pmr::monotonic_buffer_resource pool1{10000};
std::pmr::string s1{"my string", &pool1};
// 2. std::pmr::synchronized_pool_resource
// - 创建以很小的内存碎片分配内存、线程安全 的内存池
std::pmr::synchronized_pool_resource pool2;
// - 创建以很小的内存碎片分配内存、线程安全、不释放内存、按块分配内存、初始大小为10k 的内存池
std::pmr::synchronized_pool_resource pool2{&pool1};
std::pmr::string s2{"my string", &pool2};
// 2. std::pmr::unsynchronized_pool_resource
// - 创建以很小的内存碎片分配内存、线程不安全 的内存池
std::pmr::unsynchronized_pool_resource pool3;
std::pmr::string s2{"my string", &pool3};
/* 3个 返回指向单例全局内存资源的指针的函数 */
std::pmr::string s1{"my string", std::pmr::new_delete_resource()};
std::pmr::string s2{"my string", std::pmr::null_memory_resourcenew_delete_resouece()};
monotonic_buffer_resource
monotonic_buffer_resource
可以减少分配和释放操作,从而提高性能。
往往是栈上临时缓冲区,短生命周期
使用情景:当有大量的对象需要分配,且预知它们具有相似的生命周期时。
原理:在一个单调增长的内存块上进行内存分配,一直向上分配内存。当有分配内存的请求时,它会返回下一块剩余的内存,直到所有的内存都被消耗光。
-
分配:
- 传递一个缓冲区用作内存 —— 特别的,可被用来在栈上分配内存
- 再从该固定大小的缓冲区分配内存
-
不是线程安全的
-
需要确保对它的访问是同步的。
-
在使用线程池或并发场景时,要为每个线程分配一个单独的
monotonic_buffer_resource
,或者使用锁或其他同步机制来保证线程安全。
-
-
回收:内存资源从来不会释放直到整个资源作为整体被一起释放。
-
不能单独释放单个对象,只有一次性释放所有资源。
-
存在超出内存池的可能性。超出作用域时,所有通过它分配的内存都会被释放
-
内存池足够时
- 不需要任何额外的内存分配
- 内存池销毁后,内存(栈上或堆上)可被重新使用
-
内存超限时
- 会在堆里分配额外的内存
- 内存池销毁后,内存(栈上或堆上)也被释放
-
-
非常快速,因为释放操作实际上什么也不做,不需要为了复用而追踪被释放的内存
-
-
可用于让所有的内存资源跳过释放操作(我非常不建议这么使用)
-
存在
无初始内存起始地址,无内存大小
的重载版本使用默认构造时会作用于默认内存资源(即
new_delete_resource()
的返回值) -
存在
无初始内存起始地址,有内存大小
的重载版本允许传递一个初始的大小,被用作第一次内存分配的最小值
使用默认构造时会作用于默认内存资源(即
new_delete_resource()
的返回值)
-
// 限定一个作用域
{ // 使用默认的内存资源但是在池销毁之前跳过释放操作
// - 创建不释放内存、按块分配内存、未指定初始大小 的内存池
std::pmr::monotonic_buffer_resource pool2;
// - 创建不释放内存、按块分配内存、初始大小为10k 的内存池
// std::pmr::monotonic_buffer_resource pool3{10000};
// - 创建不释放内存、按块分配内存、初始大小为10k 、以很小的内存碎片分配内存 的内存池
// std::pmr::synchronized_pool_resource pool4{&pool3};
std::pmr::vector<std::pmr::string> coll{&pool};
for (int j = 0; j < 100; ++j) {
std::pmr::vector<std::pmr::string> coll{&pool};
for (int i = 0; i < 100; ++i) {
coll.emplace_back("just a non‐SSO string");
}
coll.clear(); // 销毁元素但不会释放内存
} // 底层的池收到了释放操作,但不会释放内存
// 到此为止,没有释放任何内存
} // 释放所有分配的内存
(un)synchronized_poo_resource
synchronized_pool_resource和 unsynchronized_pool_resource
-
更少碎片化
-
(un)synchronized_poo_resource
会尝试在相邻位置分配所有内存的内存资源类,可以尽可能的减小内存碎片 -
当池被销毁时它们会释放所有内存
-
一个应用方向:
-
可以保证以节点为单位的容器里的元素能相邻排列
-
这也许能显著提高容器的性能——缓存命中
实际的性能依赖于内存资源的实现。例如,如果内存资源使用了互斥量来同步内存访问,性能可能会变得非常差。
-
-
-
线程安全性(区别)
synchronized_pool_resource
线程安全(会影响性能)unsynchronized_pool_resource
线程不安全
-
封装默认的内存资源
- 这两个类从底层来看,实际的分配和释放操作都使用了默认的内存资源
- 它们只是保证内存分配更加密集的包装。
/* 这两个类使用默认的内存资源进行实际的分配和释放操作 */
std::pmr::synchronized_pool_resource myPool_sync;
// 等价于
std::pmr::synchronized_pool_resource myPool_sync{std::pmr::get_default_resource()};
std::pmr::unsynchronized_pool_resource myPool_unsync;
// 等价于
std::pmr::unsynchronized_pool_resource myPool_unsync{std::pmr::get_default_resource()};
new_delete_resource()
new_delete_resource()
的返回值
-
是默认的内存资源
不做任何修改的情况下
get_default_resource()
的返回值就是new_delete_resource()
-
处理内存分配的方式和普通的分配器相同(转发给传统 new/delete)
- 每次分配内存会调用 new
- 每次释放内存会调用 delete
持有这种内存资源的多态分配器不能和默认的分配器互换,因为它们的类型不同。
std::string s{"my string with some value"};
// 不会发生 move(直接把 s分配的内存转让给 ps)
// 而是把 s的内存拷贝到 ps内部用 new分配的新的内存中
std::pmr::string ps{std::move(s), std::pmr::new_delete_resource()};
null_memory_resource()
null_memory_resource(),对分配操作进行处理,让每一次分配都抛出 bad_alloc异常。
最主要的应用:对于在栈上分配的内存的池
- 确保其不会突然在堆上分配额外的内存
- 任何尝试分配更多堆内存的行为都会抛出异常
详情见下一章节 常用情景 - 避免误用堆内存
常用情景
修改默认内存资源
若未给pmr容器 传递内存资源,会使用多态分配器的默认内存资源
默认内存资源的操作 | 行为 |
---|---|
std::pmr::get_default_resource() | 返回指向当前默认内存资源的指针 |
std::pmr::set_default_resource() | 设置默认内存资源,返回之前的内存资源的指针 |
// std::pmr::get_default_resource() 获取当前的默认资源
// 向默认资源传递多态分配器,从而进行初始化
// - 1.初始化内存资源
static std::pmr::synchronized_pool_resource myPool;
// std::pmr::set_default_resource() 设置默认内存资源
// - 2.设置新的默认内存资源
// 之前的默认内存资源可能仍然会被使用,即使它已经被替换掉。
std::pmr::memory_resource* old = std::pmr::set_default_resource(&myPool);
// - 3.恢复旧的默认内存资源
std::pmr::set_default_resource(old)
打算在程序中设置了自定义内存资源并且把它用作默认资源
- 直接在 main()中首先将它创建为 static对象
int main() {
static std::pmr::synchronized_pool_resource myPool;
// ...
}
- 提供返回静态资源的全局函数
memory_resource* myResource() {
static std::pmr::synchronized_pool_resource myPool;
return &myPool;
}
嵌套内存池
pmr 分配器之间可以设置“上游内存池”,实现级连内存池。
// 限定一个作用域
{ // 在池销毁之前跳过释放操作
// - 创建不释放内存、按块分配内存、初始大小为10k 的内存池
std::pmr::monotonic_buffer_resource keepAllocatedPool{10000};
// 创建嵌套内存池
// - 级联不释放内存、按块分配内存、初始大小为10k 的内存池
// - 以很小的内存碎片分配内存、线程安全(会影响性能) 的内存池
std::pmr::synchronized_pool_resource pool{&keepAllocatedPool};
std::pmr::vector<std::pmr::string> coll{&pool};
for (int j = 0; j < 100; ++j) {
std::pmr::vector<std::pmr::string> coll{&pool};
for (int i = 0; i < 100; ++i) {
coll.emplace_back("just a non‐SSO string");
}
}
// 到此为止,没有释放任何内存
} // 释放所有分配的内存
复用内存池
通过复用std::pmr::monotonic_buffer_resource
内存池,可以只分配一次内存(在栈上或堆上)然后在每一个新任务里(服务请求、事件、处理数据文件等等)都复用这块内存。
但有个问题,内存超限时会额外申请堆内存,这可能会导致内存泄漏。解决方法见下一节 常用情景 - 避免误用堆内存
内存超限时,会在堆里分配额外的内存,内存池销毁后,内存(栈上或堆上)也被释放
// - 在栈上分配一些内存, 用作pmr容器的初始内存池
std::array<std::byte, 200000> buf;
// - 再次提醒:当缓冲区没有足够的内存时还会在堆上分配新的内存
for (int num : {1000, 2000, 500, 2000, 3000, 50000, 1000}) {
std::cout << "‐‐ check with " << num << " elements:\n";
TrackNew::reset();
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
std::pmr::vector<std::pmr::string> coll{&pool};
for (int i = 0; i < num; ++i) {
coll.empalce_back("just a non‐SSO string");
}
TrackNew::status();
}
避免误用堆内存
std::pmr::null_memory_resource()
最主要的应用:对于在栈上分配的内存的池
- 确保其不会突然在堆上分配额外的内存
- 任何尝试分配更多堆内存的行为都会抛出异常
int main()
{
// 创建内存池:
// - 使用栈上的内存
// - 不用堆作为备选项(备选内存资源)
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size(),
std::pmr::null_memory_resource()};
// 故意申请过量的堆内存
sdt::pmr::unordered_map <long, std::pmr::string> coll{&poll};
try {
for (int i = 0; i < buf.size(); ++i) {
std::string s{"Customer" + std::to_string(i)};
coll.emplace(i, s);
}
}
catch (const std::bad_alloc& e) {
std::cerr << "BAD ALLOC EXCEPTION: " << e.what() << '\n';
}
std::cout << "size: " << coll.size() << '\n';
}
自定义
内存资源
提供自定义内存资源,需要
-
从 std::pmr::memory_resource派生
-
实现下列私有函数:
-
分配内存
void* do_allocate(size_t bytes, size_t alignment)
-
释放内存
void do_deallocate(void* ptr, size_t bytes, size_t alignment)
-
什么情况下、何时,两个内存资源可以交换分配的内存
(即一个多态内存资源对象是否以及何时可以释放另一个多态内存资源对象分配的内存)
bool do_is_equal(const std::pmr::memory_resource& other)
-
#include <iostream>
#include <string>
#include <memory_resource >
class Tracker : public std::pmr::memory_resource
{
private:
std::pmr::memory_resource *upstream; // 被包装的内存资源
std::string prefix{};
public:
// 包装 传入的/默认的 资源:
explicit Tracker(std::pmr::memory_resource *us
= std::pmr::get_default_resource()) : upstream{us} {
}
explicit Tracker(std::string p, std::pmr::memory_resource *us
= std::pmr::get_default_resource()) : upstream{us}, prefix{std::move(p)} {
}
private:
// 分配内存
void* do_allocate(size_t bytes, size_t alignment) override {
std::cout << prefix << "allocate " << bytes << " Bytes\n";
void* ret = upstream‐>allocate(bytes, alignment);
return ret;
}
// 释放内存
void do_deallocate(void* ptr, size_t bytes, size_t alignment) override {
std::cout << prefix << "deallocate " << bytes << " Bytes\n";
upstream‐>deallocate(ptr, bytes, alignment);
}
// 何时该类型可与其他内存资源对象交换分配的内存
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
// 判断是否是同一个memory_resource对象
if (this == &other)
return true;
// 判断是否是相同的类型并且prefix和upstream都相等
auto op = dynamic_cast<const Tracker*>(&other);
return op != nullptr && op‐>prefix == prefix && upstream‐>is_equal(other);
}
};
自定义PMR类型
为自定义类型提供内存资源支持
怎么保证自己的自定义类型支持多态分配器,从而保证它们能像 pmr::string一样作为一个 pmr容器的元素?
-
指明这个类型支持多态分配器——提供类型 allocator_type的声明
定义一个 public成员 allocator_type作为一个多态分配器
-
为所有构造函数添加一个接受分配器作为额外参数的重载(包括拷贝和移动构造函数)
-
给初始化用的构造函数的分配器参数添加一个默认的 allocator_type类型的值(不包括拷贝和移动构造函数)
#include <string>
#include <memeory_resource >
// 支持多态分配器的顾客类型
// 分配器存储在字符串成员中
class PmrCustomer
{
private:
std::pmr::string name; // 可以用来存储分配器
public:
using allocator_type = std::pmr::polymorphic_allocator <char>;
// 初始化构造函数
PmrCustomer(std::pmr::string n, allocator_type alloc = {}) : name{std::move(n), alloc} {
}
// 带有分配器的移动构造函数
PmrCustomer(const PmrCustomer& c, allocator_type alloc) : name{c.name, alloc} {
}
PmrCustomer(PmrCustomer&& c, allocator_type alloc) : name{std::move(c.name), alloc} {
}
// setter/getter:
void setName(std::pmr::string s) {
name = std::move(s);
}
std::pmr::string getName() const {
return name;
}
std::string getNameAsString() const {
return std::string{name};
}
};
使用PMR类型(定义PMR容器)
#include "pmrcustomer.hpp"
#include "tracker.hpp"
#include <vector>
int main()
{
Tracker tracker;
std::pmr::vector<PmrCustomer > coll(&tracker);
coll.reserve(100); // 使用tracker分 配
PmrCustomer c1{"Peter, Paul & Mary"}; // 使用get_default_resource()分配
coll.push_back(c1); // 使用vector的分配器(tracker)分配
coll.push_back(std::move(c1)); // 拷贝(分配器不可交换)
for (const auto& cust : coll) {
std::cout << cust.getName() << '\n';
}
}
标签:std,resource,17,pmr,多态,内存,pool,string
From: https://blog.csdn.net/weixin_41733034/article/details/143664598