C++内存分配器
MemorySource
什么是内存分配器
内存分配器就是负责内存的申请和释放的一个"管家"。
// 没有管家时,你要自己管:
int* p = new int(42); // 申请
delete p; // 释放
// 有管家时,你告诉管家:
std::allocator<int> alloc; // 找个管家
int* p = alloc.allocate(1); // 管家去申请
alloc.construct(p, 42); // 管家帮忙构造
alloc.destroy(p); // 管家帮忙析构
alloc.deallocate(p, 1); // 管家去释放
所有的C++标准容器实际上都有分配其参数
// vector 的定义
template<
class T,
class Allocator = std::allocator<T> // 默认分配器
> class vector;
// list 的定义
template<
class T,
class Allocator = std::allocator<T> // 默认分配器
> class list;
// map 的定义
template<
class Key,
class T,
class Compare = less<Key>,
class Allocator = std::allocator<pair<const Key, T>> // 默认分配器
> class map;
// string 的定义
template<
class CharT,
class Traits = char_traits<CharT>,
class Allocator = std::allocator<CharT> // 默认分配器
> class basic_string;
// 日常写法:分配器参数隐藏了
std::vector<int> v1; // 等价于:
std::vector<int, std::allocator<int>> v2; // 这是完整写法
std::list<std::string> l1; // 等价于:
std::list<std::string, std::allocator<std::string>> l2;
std::map<int, std::string> m1; // 等价于:
std::map<int, std::string, std::less<int>,
std::allocator<std::pair<const int, std::string>>> m2;
根据 C++ 标准,std::allocator 通过调用 ::operator new 和 ::operator delete 来获取和释放内存 。这意味着在默认情况下,容器的内存管理最终会落到全局的 new 和 delete 操作符上。
那么什么又是pmr呢?
在早期传统的allocator中,不同的分配器成了类型的“胎记”。也就是说,即使元素的类型相同使用了不同的分配器也不能放在同一个容器中,不能够相互赋值,代码开始变得不通用。
而pmr则把分配器与类型剥离,变成运行时可以切换的对象。
简单代码展示
/* allocator */
template<typename T, typename Alloc = std::allocator<T>>
class vector {
Alloc alloc; // 分配器是类型的一部分!
// ...
};
// 当你写:
std::vector<int, MyAlloc1<int>> v1; // vector< int, MyAlloc1<int> >
std::vector<int, MyAlloc2<int>> v2; // vector< int, MyAlloc2<int> >
// 这是两个完全不同的类型!就像:
class vector_MyAlloc1 { ... };
class vector_MyAlloc2 { ... };
/* pmr */
namespace std::pmr {
template<typename T>
class polymorphic_allocator {
memory_resource* res; // 策略通过指针决定,不是类型!
public:
T* allocate(size_t n) {
return res->allocate(n * sizeof(T)); // 运行时多态
}
};
// pmr::vector 类型固定!
template<typename T>
using vector = std::vector<T, polymorphic_allocator<T>>;
}
// 所有 pmr::vector<int> 都是同一个类型:
// std::vector<int, polymorphic_allocator<int>>
实际上pmr也是allocator!自定义的allocator需要满足allocator的要求,实现对应的函数。pmr也是如此,我们自定义的pmr,实际上也是满足allocator的要求的。不过allocator把策略放在了模板参数上,类型强绑定。而pmr将策略置于内部指针中,运行时决定。
为什么需要自定义内存分配器
// 默认分配器(std::allocator)
std::vector<int> v1; // 使用 new/delete,通用但不够灵活
// 问题场景:
// 1. 大量小对象频繁分配 → 性能差、内存碎片
// 2. 实时系统 → 分配延迟不可控
// 3. 嵌入式系统 → 内存有限,需精确控制
// 4. 多个容器 → 内存无法共享
默认的内存分配器简而言之就是对普通的new/delete的管理封装,虽然C++对其进行了优化,但相对来说还是很低效的。
举个例子,假如我们在进行fps游戏,每帧渲染绘制。每次都要进行成千上万次new/delete,极其的浪费性能。如果我们能够一次性分配够足够的内存,使用的时候根据策略自己去取,那么将会更加高效。
PMR
┌─────────────────┐
│ pmr::vector │ ← 容器层(使用多态分配器)
├─────────────────┤
│polymorphic_alloc│ ← 分配器层(多态分发)
├─────────────────┤
│ memory_resource │ ← 资源层(实际内存管理)
└─────────────────┘
核心组件
namespace std::pmr {
// 1. 抽象内存资源(基类)
class memory_resource {
virtual void* do_allocate(size_t bytes, size_t align) = 0;
virtual void do_deallocate(void* p, size_t bytes, size_t align) = 0;
virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
};
// 2. 多态分配器(包装器)
template<typename T>
class polymorphic_allocator {
memory_resource* resource;
public:
T* allocate(size_t n);
void deallocate(T* p, size_t n);
};
// 3. 预定义资源
memory_resource* new_delete_resource(); // 默认 new/delete
memory_resource* null_memory_resource(); // 总是失败
memory_resource* get_default_resource(); // 获取默认资源
memory_resource* set_default_resource(); // 设置默认资源
// 4. 具体资源实现
class monotonic_buffer_resource; // 线性分配,极快
class synchronized_pool_resource; // 线程安全池
class unsynchronized_pool_resource; // 非线程安全池
}
简单而言就是一个pmr分配器需要有一个抽象的内存资源基类memory_resource,这个是实际上记录了当前资源的位置指针等。
然后是具体的分配器实现,C++默认提供的是三种
- monotonic_buffer_resource栈分配
- synchronized_pool_resource同步池
- unsynchronized_pool_resource非同步池
举个例子
char buffer[1024 * 1024]; // 1. 提供缓冲区(栈上最快)
std::pmr::monotonic_buffer_resource pool{
buffer, // 缓冲区起始地址
sizeof(buffer), // 缓冲区大小
std::pmr::null_memory_resource() // 上游(可选)
};
// 分配过程:
void* p1 = pool.allocate(100); // 从 buffer 开头拿 100 字节
void* p2 = pool.allocate(200); // 紧接着拿 200 字节
// 内存布局:[p1(100)][p2(200)][空闲]...
这样每次获取资源的时候,可以由分配器提前一次性开辟好,之后只需要取用即可。
那么标准库容器如何使用呢?
std::pmr::monotonic_buffer_resource mv3 { buffer, 65536 };
std::vector<int,std::pmr::polymorphic_allocator>vec(std::pmr::polymorphic_allocator{&mv});
// 也可以简写成为:
std::vector<int,std::pmr::polymorphic_allocator>vec(&mv);
这段代码的意思是,使用指定位置buffer的资源,大小65536,作为vec的分配器。
当然我们也可以不给出指定位置的资源,也就是不提供buffer,甚至也可以不提供大小。
区别在于,给了大小,那么容器会在第一次进行内存请求的时候一次性开辟指定大小的内存供使用;给了指定资源,直接就可以从对应的位置取用内容,无需再开辟。什么都不给,就根据标准库默认实现,在第一次取用的时候开辟一个指定大小的内存使用。如果后续不够的话,就会从上游请求内存。
比如默认情况下,上游就是std::pmr::new_delete_resource(),当资源不足的时候,系统进行new/delete分配资源。也可以指定标准库的预定义资源,比如std::pmr::null_memory_resource(),这意味着,只要你内存不足请求上游,都返回抛出std::bad_alloc异常。也可以手动的进行设置set_default_resource,get_default_resource。
剩下的两种内存池
// 特点:可复用内存,适合长期运行,小对象频繁分配
// 线程安全版本(有锁)
std::pmr::synchronized_pool_resource sync_pool;
// 非线程安全版本(无锁,更快)
std::pmr::unsynchronized_pool_resource unsync_pool;
// 可指定上游和选项
std::pmr::pool_options opts;
opts.max_blocks_per_chunk = 1024; // 每块最大块数
opts.largest_required_pool_block = 4096; // 最大块大小
std::pmr::synchronized_pool_resource pool{opts, &upstream};
简单的时间对比
char buffer[65536 * 24];
std::pmr::monotonic_buffer_resource mv { buffer, sizeof(buffer) };
std::list<int, std::pmr::polymorphic_allocator<int>> lst1 { std::pmr::polymorphic_allocator<int>(&mv) };
std::list<int> lst2;
auto begin = std::chrono::steady_clock::now();
for (int i = 0; i < 65536; i++) {
lst.push_back(i);
}
auto end = std::chrono::steady_clock::now();
auto elpased = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count();
std::cout << "耗时:" << elpased << "ns\n";
begin = std::chrono::steady_clock::now();
for (int i = 0; i < 65536; i++) {
lst2.push_back(i);
}
end = std::chrono::steady_clock::now();
elpased = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count();
std::cout << "耗时:" << elpased << "ns\n";
一个使用了std::pmr::monotonic_buffer_resource栈上现行分配资源,另一个默认是普通的new/delete分配。
差距如下,有两倍之多。
耗时:683171ns
耗时:1434197ns
手动实现一个简单的pmr分配器
struct my_memory_resource {
char* buf ;
std::size_t watermark = 0;
char* allocate(size_t n, size_t align)
{
watermark = (watermark + align - 1) / align * align;
char* p = buf + watermark;
if (watermark + n > sizeof(buffer)) {
throw std::bad_alloc();
}
watermark += n;
return p;
}
void release() noexcept
{
watermark = 0;
}
};
template <class T>
struct my_allocator {
using value_type = T;
std::shared_ptr<my_memory_resource> m_resource = nullptr;
my_allocator() = default;
my_allocator(std::shared_ptr<my_memory_resource> resouece)
: m_resource(resouece)
{
}
T* allocate(size_t n)
{
char* p = m_resource->allocate(n * sizeof(T), alignof(T));
return (T*)(p);
}
void deallocate(T* p, size_t n)
{
// do nothing
}
void release() noexcept
{
m_resource->release();
}
template <class U>
constexpr my_allocator(const my_allocator<U>& other) noexcept
: m_resource(other.m_resource)
{
}
constexpr bool operator==(const my_allocator& other) noexcept
{
return m_resource == other.m_resource;
}
};
首先是my_memory_resource,里面存放管理了内存的位置以及水位高低(watermark).
而my_allocator则是根据标准库满足Allocator结构的分配器,比如实现了allocate、deallocate、拷贝构造,operator==函数,以及value_type类型,这是必须要实现的。
比如一个容器使用了我们的自定义分配器,每次内存请求分配的时候,就会调用allocate函数,而allocate函数内部通过my_memory_source管理了水位以及buffer.
使用我们的自定义分配器如下
char buffer[65536 * 24];
auto mv = std::make_shared<my_memory_resource>(buffer);
std::list<int,my_allocator<int>>list(my_allocator<int>{mv});
通过测试,使用默认分配器,标准库分配器和自定义分配器的list容器耗时如下
耗时:1868331ns
耗时:860653ns
耗时:638812ns
由于标准库的分配器内部有许多的安全判断以及转发,所以性能略低于自定义分配器。
实现内存日志
#pragma once
#include <fmt/format.h>
struct memory_resource_inspector : std::pmr::memory_resource {
public:
explicit memory_resource_inspector(std::pmr::memory_resource *upstream)
: m_upstream(upstream) {}
private:
void *do_allocate(size_t bytes, size_t alignment) override {
void *p = m_upstream->allocate(bytes, alignment);
fmt::print("allocate {} {} {}\n", p, bytes, alignment);
return p;
}
bool do_is_equal(std::pmr::memory_resource const &other) const noexcept override {
return other.is_equal(*m_upstream);
}
void do_deallocate(void *p, size_t bytes, size_t alignment) override {
fmt::print("deallocate {} {} {}\n", p, bytes, alignment);
return m_upstream->deallocate(p, bytes, alignment);
}
std::pmr::memory_resource *m_upstream;
};
在这里我们所有的分配策略底层使用默认的new/delete进行分配,但是通过我们这个自定义的分配器进行管理。每次allocate内存请求分配的时候,我们可以打印记录,每次deallocate的时候同理。
测试如下
int main() {
memory_resource_inspector mem{std::pmr::new_delete_resource()};
std::pmr::vector<int> s{&mem};
for (int i = 0; i < 4096; i++) {
s.push_back(i);
}
}
❯ "/home/vivek/Codes/some_implementations/new_features/out/build/GCC 15.2.1 x86_64-pc-linux-gnu/new_features"
allocate 0x559298c5c020 4 4
allocate 0x559298c5c780 8 4
deallocate 0x559298c5c020 4 4
allocate 0x559298c5c020 16 4
deallocate 0x559298c5c780 8 4
allocate 0x559298c5c040 32 4
deallocate 0x559298c5c020 16 4
allocate 0x559298c5c7d0 64 4
deallocate 0x559298c5c040 32 4
allocate 0x559298c5c820 128 4
deallocate 0x559298c5c7d0 64 4
allocate 0x559298c5c8b0 256 4
deallocate 0x559298c5c820 128 4
allocate 0x559298c5c9c0 512 4
deallocate 0x559298c5c8b0 256 4
allocate 0x559298c5cbd0 1024 4
deallocate 0x559298c5c9c0 512 4
allocate 0x559298c5cfe0 2048 4
deallocate 0x559298c5cbd0 1024 4
allocate 0x559298c5d7f0 4096 4
deallocate 0x559298c5cfe0 2048 4
allocate 0x559298c5e800 8192 4
deallocate 0x559298c5d7f0 4096 4
allocate 0x559298c60810 16384 4
deallocate 0x559298c5e800 8192 4
deallocate 0x559298c60810 16384 4

浙公网安备 33010602011771号