第三章 内存管理:从内存池到大页内存的实战之路
内存管理,说白了就是交易网关的命脉。我见过太多系统,算法写得再漂亮,一到高并发场景就卡在内存分配上。今天咱们就把这块硬骨头啃下来。
3.1 为什么内存管理如此重要?
你想想看,一个极速交易网关,每秒要处理几万笔订单。如果每笔订单都去调用 malloc/free,那性能损耗有多大?
我在项目中遇到过这样一个案例:某券商的生产环境,行情数据一来,系统延迟从 5 微秒直接飙升到 200 微秒。查了半天,发现是内存分配器在高并发下频繁加锁导致的。嗯,这就是典型的「内存分配瓶颈」。
内存管理的核心目标有三个:
- 减少系统调用:避免频繁的 malloc/free
- 避免内存碎片:防止长时间运行后内存利用率下降
- 提高缓存命中率:让数据尽量靠近 CPU
3.2 内存池设计:从零开始造轮子
我个人习惯,在交易网关里一定会实现一个定制的内存池。为什么不用现成的?因为通用内存池要考虑各种场景,而我们只需要针对订单、行情、报单等固定大小的对象做优化。
3.2.1 固定大小内存池
最简单的内存池,就是预先分配一大块内存,切成固定大小的块。每次分配直接拿一块,释放就还回去。
// 一个简单的固定大小内存池
class FixedSizePool {
private:
struct Block {
Block* next; // 空闲链表指针
};
Block* free_list_;
char* pool_;
size_t block_size_;
size_t pool_size_;
public:
FixedSizePool(size_t block_size, size_t block_count)
: block_size_(block_size), pool_size_(block_count) {
// 一次性分配大块内存
pool_ = new char[block_size * block_count];
// 初始化空闲链表
free_list_ = reinterpret_cast<Block*>(pool_);
Block* current = free_list_;
for (size_t i = 1; i < block_count; ++i) {
current->next = reinterpret_cast<Block*>(pool_ + i * block_size);
current = current->next;
}
current->next = nullptr;
}
void* allocate() {
if (!free_list_) return nullptr;
Block* block = free_list_;
free_list_ = free_list_->next;
return block;
}
void deallocate(void* ptr) {
Block* block = reinterpret_cast<Block*>(ptr);
block->next = free_list_;
free_list_ = block;
}
~FixedSizePool() {
delete[] pool_;
}
};
这个实现虽然简单,但已经能解决 90% 的问题了。分配和释放都是 O(1) 操作,没有锁竞争,没有系统调用。
3.2.2 多级内存池
交易网关里,不同对象的大小差异很大。订单可能 128 字节,行情快照可能 256 字节,报单回执可能 64 字节。怎么办?
我一般会设计一个多级内存池,按 2 的幂次划分大小:
class MultiLevelPool {
private:
static const int MAX_LEVEL = 16; // 最大支持 2^16 字节
FixedSizePool* pools_[MAX_LEVEL];
int get_level(size_t size) {
// 计算需要的级别
int level = 0;
size_t s = 1;
while (s < size) {
s <<= 1;
++level;
}
return level;
}
public:
MultiLevelPool() {
for (int i = 0; i < MAX_LEVEL; ++i) {
size_t block_size = 1 << i; // 2^i
pools_[i] = new FixedSizePool(block_size, 1024);
}
}
void* allocate(size_t size) {
int level = get_level(size);
return pools_[level]->allocate();
}
void deallocate(void* ptr, size_t size) {
int level = get_level(size);
pools_[level]->deallocate(ptr);
}
};
3.3 避免内存碎片:实战中的血泪教训
我曾经在一个高频交易系统里,连续运行了 72 小时后,内存碎片率达到了 40%。系统开始频繁分配失败,交易被迫中断。那次事故让我深刻认识到:内存碎片不是理论问题,是真会要命的。
内存碎片主要分两种:
- 外部碎片:空闲内存总量够,但没有连续的大块
- 内部碎片:分配的内存比实际需要的大
避免碎片的核心策略:
- 对象池化:所有对象从池中分配,不释放回系统
- 固定大小分配:避免大小不一的内存请求
- 批量回收:不是用完就释放,而是攒一批再回收
3.4 大页内存(HugePages):让 TLB 命中率飙升
为什么需要大页?
CPU 访问内存时,需要通过 TLB(页表缓存)把虚拟地址转成物理地址。默认的 4KB 页大小,意味着 2MB 的 L2 TLB 只能覆盖 512 个页。如果工作集超过 2MB,TLB 就会频繁 miss。
大页内存(2MB 或 1GB)能显著减少页表项数量。2MB 的大页,同样的 TLB 覆盖范围是原来的 512 倍。
3.4.1 配置大页内存
在 Linux 上配置大页:
# 查看当前大页配置
cat /proc/meminfo | grep Huge
# 配置 1024 个 2MB 的大页(总共 2GB)
echo 1024 > /proc/sys/vm/nr_hugepages
# 永久配置(在 /etc/sysctl.conf 中添加)
vm.nr_hugepages = 1024
3.4.2 在代码中使用大页
#include <sys/mman.h>
#include <fcntl.h>
void* allocate_huge_pages(size_t size) {
// 使用 mmap 分配大页内存
void* ptr = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
if (ptr == MAP_FAILED) {
// 回退到普通内存
ptr = malloc(size);
}
return ptr;
}
// 使用大页初始化内存池
class HugePagePool {
private:
void* pool_;
size_t pool_size_;
public:
HugePagePool(size_t size) : pool_size_(size) {
pool_ = allocate_huge_pages(size);
if (!pool_) {
throw std::bad_alloc();
}
}
~HugePagePool() {
if (pool_) {
munmap(pool_, pool_size_);
}
}
};
* soft memlock unlimited* hard memlock unlimited
3.5 内存对齐与缓存行填充
现代 CPU 读取内存是以缓存行为单位的(通常是 64 字节)。如果两个线程频繁访问的数据在同一个缓存行里,就会产生「伪共享」问题——明明操作的是不同变量,却要互相等待缓存同步。
3.5.1 内存对齐
// 使用 alignas 指定对齐
struct alignas(64) CacheLineAligned {
int32_t value;
char padding[60]; // 填充到 64 字节
};
// 或者使用 C++17 的 std::hardware_destructive_interference_size
struct alignas(std::hardware_destructive_interference_size)
AtomicCounter {
std::atomic<int64_t> counter;
};
3.5.2 缓存行填充实战
在交易网关中,每个订单的读写频率极高。如果多个订单的数据挤在一个缓存行里,性能会急剧下降。
// 订单结构体,每个订单独占一个缓存行
struct alignas(64) Order {
int64_t order_id;
int32_t price;
int32_t quantity;
int32_t status;
char padding[40]; // 填充到 64 字节
};
// 订单数组,每个元素对齐到缓存行
Order orders[1024]; // 每个 Order 正好 64 字节
3.6 知识体系总览
下面这张图总结了本章的核心内容:
3.7 总结
内存管理这块,说白了就是「空间换时间」。预分配、池化、对齐、大页,每个技术都是在用内存空间换取执行效率。
我个人建议,在交易网关项目中,至少要做到:
- 所有高频分配的对象都用内存池管理
- 关键数据结构对齐到缓存行
- 大页内存用于核心数据区
- 定期监控内存碎片率
做到这几点,你的交易网关在内存方面就不会出大问题。嗯,今天就到这里。