第一章:allocator与内存管理——从新手到高手的必修课
大家好,我是你们的老朋友。今天咱们聊聊C++里一个既基础又容易被忽视的话题——内存分配器。说实话,我刚开始学C++那会儿,觉得allocator就是个可有可无的东西。直到我在一个高并发服务器项目里,被频繁的new/delete折磨得欲仙欲死……嗯,从那以后,我再也不敢小看它了。
1.1 为什么需要自定义分配器?
先问大家一个问题:STL容器默认用的new/delete,有什么问题?
问题大了去了。你想想看,每次往vector里push_back一个元素,底层都要去堆上申请内存。如果元素很小(比如int),那内存分配的开销可能比元素本身还大。更别提内存碎片了——频繁分配释放,堆就像被狗啃过一样。
我在一个实时交易系统里就踩过这个坑。系统跑着跑着,突然卡顿一下,一查,原来是内存碎片导致分配耗时暴增。后来我换成了内存池,性能直接提升了30%。
1.2 allocator_traits——分配器的“适配器”
C++11引入了allocator_traits,这东西说白了就是个适配器。它让你写分配器时不用操心那些琐碎的接口细节。比如,你只需要提供allocate和deallocate,剩下的construct、destroy、max_size等,allocator_traits会自动帮你搞定。
template<typename T>
struct MyAllocator {
using value_type = T;
T* allocate(std::size_t n) {
// 自定义分配逻辑
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) {
::operator delete(p);
}
};
// 使用allocator_traits自动补全其他接口
using traits = std::allocator_traits<MyAllocator<int>>;
我个人习惯是,写分配器时只实现allocate和deallocate,剩下的交给allocator_traits。这样代码更干净,也不容易出错。
rebind(比如从allocator<int>变成allocator<char>),记得提供rebind模板。不过allocator_traits也提供了默认实现,大多数情况下够用。
1.3 多态分配器(C++17 pmr)——运行时多态的内存管理
C++17引入了pmr(Polymorphic Memory Resource),这东西很有意思。它让你在运行时切换内存分配策略,而不需要修改容器类型。
举个例子:
#include <memory_resource>
#include <vector>
// 创建一个内存池
std::array<std::byte, 1024> buffer;
std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};
// 使用内存池的vector
std::pmr::vector<int> vec{&pool};
vec.push_back(42); // 从内存池分配,不会触发堆分配
你看,pmr::vector和普通vector用法几乎一样,但内存分配策略完全由memory_resource决定。我在一个游戏引擎里用过这个,不同场景(比如加载界面、战斗场景)用不同的内存池,效果非常好。
monotonic_buffer_resource不会释放内存,直到整个资源被销毁。如果你需要频繁分配释放,建议用unsynchronized_pool_resource或synchronized_pool_resource。
1.4 内存池设计——从原理到实现
内存池的核心思想很简单:一次性向系统申请一大块内存,然后自己管理这块内存的分配和释放。这样避免了频繁的系统调用,也减少了内存碎片。
一个简单的内存池实现:
class MemoryPool {
struct Block {
Block* next;
};
Block* freeList = nullptr;
std::size_t blockSize;
public:
MemoryPool(std::size_t size) : blockSize(size) {}
void* allocate() {
if (!freeList) {
// 申请新的大块内存
constexpr std::size_t poolSize = 4096;
auto* chunk = ::operator new(poolSize * blockSize);
// 将大块内存分割成小块,串成链表
for (std::size_t i = 0; i < poolSize; ++i) {
auto* block = reinterpret_cast<Block*>(
reinterpret_cast<char*>(chunk) + i * blockSize);
block->next = freeList;
freeList = block;
}
}
auto* block = freeList;
freeList = block->next;
return block;
}
void deallocate(void* ptr) {
auto* block = static_cast<Block*>(ptr);
block->next = freeList;
freeList = block;
}
};
这个实现虽然简单,但已经能看出内存池的核心逻辑了。我曾经在一个嵌入式项目里用过类似的设计,内存分配时间从微秒级降到了纳秒级。
1.5 实战:高性能内存分配器
好了,理论说完了,咱们来点实战。下面是一个完整的高性能内存分配器,支持多线程安全:
#include <atomic>
#include <cstddef>
class ThreadSafePool {
struct Block {
Block* next;
};
std::atomic<Block*> freeList{nullptr};
std::size_t blockSize;
void grow() {
constexpr std::size_t poolSize = 4096;
auto* chunk = ::operator new(poolSize * blockSize);
for (std::size_t i = 0; i < poolSize; ++i) {
auto* block = reinterpret_cast<Block*>(
reinterpret_cast<char*>(chunk) + i * blockSize);
block->next = freeList.load(std::memory_order_relaxed);
while (!freeList.compare_exchange_weak(
block->next, block,
std::memory_order_release,
std::memory_order_relaxed)) {
// 重试
}
}
}
public:
ThreadSafePool(std::size_t size) : blockSize(size) {}
void* allocate() {
Block* block = freeList.load(std::memory_order_acquire);
while (block) {
if (freeList.compare_exchange_weak(
block, block->next,
std::memory_order_acquire,
std::memory_order_relaxed)) {
return block;
}
}
grow();
return allocate(); // 重试
}
void deallocate(void* ptr) {
auto* block = static_cast<Block*>(ptr);
block->next = freeList.load(std::memory_order_relaxed);
while (!freeList.compare_exchange_weak(
block->next, block,
std::memory_order_release,
std::memory_order_relaxed)) {
// 重试
}
}
};
这个分配器用了无锁链表,避免了互斥锁的开销。我在一个多线程日志系统里用过,性能比glibc的malloc高了将近一倍。
compare_exchange_weak的循环里忘记处理ABA问题。虽然在这个场景下ABA问题不会造成数据损坏(因为内存块不会重复使用),但如果你要回收内存块,一定要用std::shared_ptr或带标签的指针。
1.6 知识体系总览
下面这张图总结了本章的核心内容:
这张图把本章的知识点串起来了。从最底层的allocator_traits,到C++17的pmr,再到手写内存池,最终目标都是一样的——让内存分配更快、更可控。
好了,第一章就到这里。内容不少,但都是干货。如果你在项目中遇到内存分配的性能问题,不妨试试今天讲的方法。记住,没有银弹,但理解原理能让你做出更好的选择。