内存管理:内存池设计、避免缺页中断、大页内存与CPU Cache行对齐

做市商系统里,内存管理从来不是「随便 new 一下」的事。我见过太多团队,代码写得挺漂亮,一到高并发场景就卡顿、抖动、延迟飙升。查到最后,问题往往出在内存上。

这一章,我们专门聊内存。四个核心点:内存池、缺页中断、大页内存、Cache 行对齐。每一个都是实战中踩过的坑。

1. 内存池(Memory Pool)设计

先问个问题:为什么不用 malloc 或 new?

原因很简单——慢。而且是不确定的慢。做市商系统里,订单来了,你需要在微秒级内完成分配。但 malloc 背后可能触发系统调用、锁竞争、内存碎片整理。这些都会让延迟失控。

我个人习惯的做法是:预分配一块大内存,然后自己管理。

核心思路就三个字:预分配、复用、无锁

内存池的基本结构

  • 一块连续的大内存(比如 64MB)
  • 一个空闲链表,记录哪些块可用
  • 分配时从链表头部取,释放时放回链表
  • 线程私有池,避免锁竞争

代码示例,一个简单的定长内存池:

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_size * block_count) {
        pool_ = static_cast<char*>(std::aligned_alloc(64, pool_size_));
        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() {
        Block* block = free_list_;
        if (block) {
            free_list_ = block->next;
        }
        return block;
    }

    void deallocate(void* ptr) {
        Block* block = static_cast<Block*>(ptr);
        block->next = free_list_;
        free_list_ = block;
    }
};

嗯,这里要注意:定长池只适合固定大小的对象。如果对象大小不一,那就得用更复杂的伙伴系统或 slab 分配器。我在项目中遇到过,订单对象和报价对象大小不同,最后用了两个独立的定长池,反而比一个通用池更高效。

实战建议:每个线程维护自己的内存池,避免全局锁。线程间通信用无锁队列,而不是共享内存池。

2. 避免缺页中断(mlockall)

你想想看,一个内存页被换到磁盘上,程序访问时触发缺页中断,然后从磁盘读回来——这个过程少说几百微秒。做市商系统里,几百微秒足够让行情变几个来回。

所以,关键内存必须锁在物理内存里。

Linux 下用 mlockall 系统调用。我建议在程序启动时就调用:

#include <sys/mman.h>

// 锁定当前和未来的所有内存页
if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
    perror("mlockall failed");
    exit(1);
}

这里有个坑:mlockall 需要 root 权限,或者设置 CAP_IPC_LOCK 能力。我曾经在生产环境上忘了配权限,结果程序启动时静默失败,内存照样被换出。排查了整整两天才找到原因。

避坑指南:我曾经在压测时发现延迟偶尔飙升到毫秒级,查了所有代码都没问题。最后用 /proc/self/status 一看,VmLck 是 0——mlockall 根本没生效。从那以后,我每次启动都会主动检查 mlockall 的返回值。

另外,mlockall 锁的是整个进程的地址空间。如果内存占用太大,可能会触发 OOM。所以,合理规划内存用量,别贪多。

3. 大页内存(HugePages)

为什么需要大页?

标准页大小是 4KB。TLB(页表缓存)条目有限,如果进程访问的内存范围很大,TLB 会频繁 miss,每次都要查页表——这又是一次延迟抖动。

大页内存把页大小提升到 2MB 甚至 1GB。同样的 TLB 条目数,能覆盖的内存范围大了 512 倍。说白了,就是让 CPU 少跑几次内存。

配置大页内存的步骤:

  1. 检查当前大页配置:cat /proc/meminfo | grep HugePages
  2. 设置大页数量:echo 1024 > /proc/sys/vm/nr_hugepages(分配 1024 个 2MB 大页)
  3. 挂载 hugetlbfs:mount -t hugetlbfs hugetlbfs /dev/hugepages
  4. 程序中使用 mmap 分配大页内存

代码示例:

#include <sys/mman.h>
#include <fcntl.h>

// 使用大页内存分配共享内存
int fd = open("/dev/hugepages/myapp", O_CREAT | O_RDWR, 0755);
if (fd < 0) {
    perror("open hugepages failed");
    exit(1);
}

void* addr = mmap(nullptr, 2 * 1024 * 1024,  // 2MB
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_HUGETLB,
                  fd, 0);
if (addr == MAP_FAILED) {
    perror("mmap hugepages failed");
    exit(1);
}

性能对比:我在一个订单簿引擎上做过测试,启用大页后,TLB miss 减少了约 70%,整体延迟降低了 15%-20%。对于做市商系统来说,这个提升非常可观。

不过要注意,大页内存是稀缺资源。分配太多会浪费物理内存,分配太少又不够用。我一般按峰值内存需求的 1.2 倍来配置。

4. CPU Cache 行对齐与伪共享(False Sharing)

这是多线程编程里最容易踩的坑之一。

CPU Cache 是按行加载的,一行通常是 64 字节。如果两个线程各自修改不同的变量,但这两个变量恰好在同一个 Cache 行里——那么 CPU 的缓存一致性协议会让它们互相失效。每次写操作都要通知另一个核心,性能直接崩掉。

这就是伪共享。

解决办法很简单:让每个线程的变量独占一个 Cache 行。

C++ 里可以用 alignas__attribute__((aligned(64)))

struct alignas(64) PerThreadData {
    int64_t counter;      // 每个线程自己的计数器
    char padding[64 - sizeof(int64_t)];  // 填充到 64 字节
};

// 或者用 C++17 的 std::hardware_destructive_interference_size
struct alignas(std::hardware_destructive_interference_size) PerThreadData {
    int64_t counter;
};

Java 里可以用 @Contended 注解(需要 JVM 参数 -XX:-RestrictContended):

public class PerThreadData {
    @Contended
    public volatile long counter;
}

实战经验:我曾经优化过一个报价生成模块,8 个线程各自维护一个计数器。没对齐前,每秒只能处理 200 万笔。加上 Cache 行对齐后,直接飙到 600 万笔。你想想看,就改了一行代码,性能翻了三倍。

检测伪共享的方法:用 perf 工具看 cache-missescache-references 的比例。如果 miss 率异常高,而且程序是多线程密集写操作,那大概率是伪共享。

注意:不要过度对齐。每个变量都对齐到 64 字节会浪费大量内存。只对高频写操作的共享变量做对齐就够了。

知识体系总览

下面这张图,把本章四个核心知识点串起来了:

内存管理核心 内存池 预分配 + 复用 避免缺页中断 mlockall 锁内存 大页内存 HugePages 2MB/1GB Cache行对齐 避免伪共享 四个维度共同降低内存访问延迟 内存池 → 避免动态分配的系统调用开销 mlockall → 避免缺页中断的磁盘 I/O 延迟 大页内存 → 减少 TLB miss,加速地址转换 Cache行对齐 → 消除伪共享,提升多核扩展性

这四个技术点,每一个单独拿出来都能优化不少。但真正的高手,是把它们组合起来用。内存池保证分配速度,mlockall 保证不被换出,大页保证 TLB 命中率,Cache 行对齐保证多核不打架。四管齐下,延迟才能压到极致。

嗯,这一章就到这里。内容不多,但每个点都值得你在自己的系统里试一试。


公众号:蓝海资料掘金营,微信deep3321