哈喽,各位好!欢迎来到今天的C++“内存历险记”!今天我们要聊的是一个听起来有点高大上,但其实用起来能让你的代码更灵活、更高效的家伙:std::pmr::polymorphic_allocator
,也就是运行时多态内存分配器。
第一幕:内存分配的老故事
在开始“多态之旅”之前,我们先简单回顾一下传统的内存分配方式。想象一下,你是一家餐厅的老板,客人来了要点菜,你得给他们准备食材。
new/delete
(malloc/free): 这就像你自己去菜市场买菜。你直接跟市场大妈说:“我要一块猪肉!”市场大妈给你一块,用完你得自己再拿回去还给人家。这种方式简单粗暴,但效率不高,而且容易出错(比如忘记还了,造成内存泄漏)。
int* arr = new int[10]; // 买10个int大小的“猪肉”
// ... 使用 arr ...
delete[] arr; // 还给市场大妈
- 定制分配器: 如果你觉得市场大妈太慢,你可以自己开个农场,专门给自己餐厅供菜。这就是定制分配器。你可以根据自己的需求优化内存分配策略,比如预先分配一大块内存,然后从中切分给客人。
#include <memory>
// 一个简单的定制分配器,分配的内存会打印信息
template <typename T>
class MyAllocator {
public:
using value_type = T;
MyAllocator() noexcept {}
template <typename U>
MyAllocator(const MyAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
std::cout << "分配 " << n * sizeof(T) << " 字节的内存" << std::endl;
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) {
std::cout << "释放 " << n * sizeof(T) << " 字节的内存" << std::endl;
::operator delete(p);
}
};
template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept { return true; }
template <typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept { return false; }
int main() {
std::vector<int, MyAllocator<int>> vec(10); // 使用自定义分配器
vec[0] = 1;
return 0;
}
定制分配器虽然高效,但也有个问题:它和特定的数据结构紧密耦合。如果你的餐厅要换一种菜系,你的农场可能就得重新规划了。
第二幕:pmr::polymorphic_allocator
闪亮登场!
现在,让我们请出今天的明星:std::pmr::polymorphic_allocator
。它就像一个超级灵活的食材供应商,可以根据不同的菜系(数据结构)提供不同的食材(内存分配策略)。
- 多态性:
polymorphic_allocator
具有多态性,这意味着它可以指向不同的内存资源(也就是memory_resource
)。你可以根据需要在运行时切换内存资源,而不需要重新编译代码。 memory_resource
:memory_resource
是一个抽象基类,定义了内存分配和释放的接口。你可以创建自己的memory_resource
实现,来使用不同的内存分配策略,比如堆分配、栈分配、内存池等等。
#include <memory_resource>
#include <vector>
#include <iostream>
int main() {
// 使用默认的堆分配器
std::pmr::polymorphic_allocator<int> alloc1;
std::pmr::vector<int> vec1(alloc1);
// 创建一个简单的内存池
std::array<char, 1024> buffer;
std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
std::pmr::polymorphic_allocator<int> alloc2(&pool); //allocator指向pool
std::pmr::vector<int> vec2(alloc2);
// 现在 vec2 使用内存池进行分配
for (int i = 0; i < 100; ++i) {
vec2.push_back(i); //push_back操作调用alloc2的allocate函数,最终从pool分配内存
}
std::cout << "vec2 size: " << vec2.size() << std::endl;
return 0;
}
第三幕:memory_resource
的家族成员
memory_resource
本身是个抽象类,但标准库提供了一些常用的实现:
-
new_delete_resource
: 这是默认的内存资源,使用new/delete
进行分配和释放。就像我们一开始说的菜市场大妈。 -
null_memory_resource
: 这是一个空洞的内存资源,分配总是失败。可以用来测试代码在内存分配失败时的行为。 -
monotonic_buffer_resource
: 这是一个单调递增的缓冲区资源。它从预先分配好的缓冲区中分配内存,一旦分配就不能释放。适合用于生命周期较短的对象,可以避免频繁的内存分配和释放。就像一个自助餐厅,你只能拿东西,不能退回去。 -
synchronized_pool_resource
和unsynchronized_pool_resource
: 这两个是内存池资源,可以管理多个大小不同的内存块。synchronized_pool_resource
是线程安全的,而unsynchronized_pool_resource
则不是。就像一个仓库,你可以根据需要存放不同大小的货物。
表格:memory_resource
家族成员
类名 | 描述 | 线程安全 | 优点 | 缺点 |
---|---|---|---|---|
std::pmr::new_delete_resource |
使用 new/delete 进行内存分配和释放。 |
是 | 简单易用,是默认的内存资源。 | 性能可能不是最优,容易出现内存碎片。 |
std::pmr::null_memory_resource |
总是返回空指针,模拟内存分配失败的情况。 | 是 | 可以用来测试代码在内存分配失败时的行为。 | 不能实际分配内存。 |
std::pmr::monotonic_buffer_resource |
从预先分配好的缓冲区中单调递增地分配内存,不能释放。 | 否 | 速度快,避免了频繁的内存分配和释放,适合用于生命周期较短的对象。 | 不能释放已经分配的内存,容易造成内存浪费。 |
std::pmr::synchronized_pool_resource |
维护一个内存池,可以分配和释放不同大小的内存块。线程安全。 | 是 | 可以减少内存碎片,提高内存利用率。 | 开销较大,需要维护内存池的元数据。 |
std::pmr::unsynchronized_pool_resource |
维护一个内存池,可以分配和释放不同大小的内存块。非线程安全。 | 否 | 性能比 synchronized_pool_resource 更好,适合单线程环境。 |
非线程安全,不适合多线程环境。 |
第四幕:实战演练:打造你的专属内存池
光说不练假把式,让我们来创建一个简单的内存池,并用它来分配内存。
#include <memory_resource>
#include <vector>
#include <iostream>
#include <algorithm>
class SimpleMemoryPool : public std::pmr::memory_resource {
public:
SimpleMemoryPool(size_t poolSize) : poolSize_(poolSize), currentOffset_(0) {
pool_ = new char[poolSize];
}
~SimpleMemoryPool() override {
delete[] pool_;
}
protected:
void* do_allocate(size_t bytes, size_t alignment) override {
//对齐内存
size_t alignedOffset = (currentOffset_ + alignment - 1) & ~(alignment - 1);
if (alignedOffset + bytes > poolSize_) {
std::cerr << "内存池空间不足!" << std::endl;
return nullptr;
}
void* ptr = pool_ + alignedOffset;
currentOffset_ = alignedOffset + bytes;
return ptr;
}
void do_deallocate(void* p, size_t bytes, size_t alignment) override {
// 内存池不支持单独释放,只能整体释放
// 在实际应用中,你可以根据需要实现更复杂的释放策略
std::cout << "内存池不支持单独释放,忽略释放请求。" << std::endl;
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
// 简单的比较,直接比较指针
return this == &other;
}
private:
char* pool_;
size_t poolSize_;
size_t currentOffset_;
};
int main() {
// 创建一个 1024 字节的内存池
SimpleMemoryPool pool(1024);
// 创建一个使用内存池的 polymorphic_allocator
std::pmr::polymorphic_allocator<int> alloc(&pool);
// 创建一个使用 allocator 的 vector
std::pmr::vector<int> vec(alloc);
// 向 vector 中添加一些元素
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
std::cout << "vec size: " << vec.size() << std::endl;
//使用alloc分配内存
int* ptr = alloc.allocate(1);
*ptr = 42;
std::cout << "*ptr: " << *ptr << std::endl;
alloc.deallocate(ptr,1); // 内存池不支持单个释放,会打印提示信息
return 0;
}
在这个例子中,我们创建了一个简单的 SimpleMemoryPool
类,它继承自 std::pmr::memory_resource
。do_allocate
方法负责从内存池中分配内存,do_deallocate
方法负责释放内存。注意,在这个简单的例子中,我们并不真正释放内存,只是打印一条消息。
第五幕:pmr::string
和 pmr::vector
:好基友,一辈子
polymorphic_allocator
最常用的场景是和 std::pmr::string
和 std::pmr::vector
等容器一起使用。这些容器都接受一个 allocator
参数,你可以将 polymorphic_allocator
传递给它们,从而控制它们的内存分配行为。
#include <memory_resource>
#include <string>
#include <vector>
#include <iostream>
int main() {
// 创建一个简单的内存池
std::array<char, 2048> buffer;
std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
// 创建一个使用内存池的 pmr::string
std::pmr::string str(&pool);
str = "Hello, world!";
std::cout << "str: " << str << std::endl;
// 创建一个使用内存池的 pmr::vector
std::pmr::vector<int> vec({1, 2, 3}, &pool);
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,我们创建了一个 monotonic_buffer_resource
内存池,然后使用它来分配 pmr::string
和 pmr::vector
的内存。这样,string
和 vector
就可以共享同一个内存池,从而提高内存利用率。
第六幕:性能考量:别盲目追求“高大上”
polymorphic_allocator
提供了很大的灵活性,但也带来了一些额外的开销。
- 虚函数调用:
memory_resource
使用虚函数来实现内存分配和释放,这会带来一定的性能损失。 - 间接性: 通过
polymorphic_allocator
分配内存需要经过memory_resource
的中转,这也会增加一定的开销。
因此,在使用 polymorphic_allocator
时,需要仔细权衡灵活性和性能。如果你的代码对性能要求非常高,或者你的内存分配策略非常简单,那么使用传统的 new/delete
或定制分配器可能更合适。
第七幕:最佳实践:让 polymorphic_allocator
为你服务
- 避免频繁切换内存资源: 频繁切换
polymorphic_allocator
的memory_resource
会带来额外的开销,尽量避免这种情况。 - 使用内存池: 内存池可以减少内存碎片,提高内存利用率,适合用于分配大量小型对象。
- 自定义
memory_resource
: 如果你需要更精细的内存控制,可以自定义memory_resource
实现。 - 性能测试: 在使用
polymorphic_allocator
之前,一定要进行性能测试,确保它能够满足你的需求。
第八幕:总结陈词
std::pmr::polymorphic_allocator
是一个强大的工具,可以让你更加灵活地控制内存分配。它通过提供多态性,让你可以在运行时切换不同的内存资源,从而适应不同的需求。但是,polymorphic_allocator
也带来了一些额外的开销,因此在使用时需要仔细权衡灵活性和性能。
希望今天的“内存历险记”能够帮助你更好地理解 std::pmr::polymorphic_allocator
。记住,选择合适的内存分配策略就像选择合适的食材一样,只有根据实际情况做出正确的选择,才能做出美味佳肴(高效的代码)!
谢谢大家!