跳转至

嵌入式 C++ 教程:Slab / Arena 实现与比较

这里开始的是使用固定池 / Slab / Arena来展开我们涉及到内存分配中剩下的一些内容——SlabArena(Bump / Region)。当然,部分内容实际上已经是操作系统的层次的知识了,但是知道了总不赖!

TL;DR

  • 固定池:固定大小对象、超低碎片、常用于驱动/对象池;实现简单,分配和释放都是 O(1)。
  • Slab:内核/复杂嵌入式系统常用,支持多种对象 size-class,减少内存碎片,易于缓存局部性优化。
  • Arena(Region):非常适合短生命周期对象或一次性分配的场景(例如解析、启动阶段),分配快(仅移动指针),回收一次性做 reset()

Slab 分配器

Linux kernel 的 slab 概念特别适合多种大小对象的优化:为每个 size-class 维护一个或多个 slab(本质上就是一组固定大小的对象池),并且可以跟踪对象使用情况,做对象构造/销毁优化(缓存 warm objects)。虽然单片机可能不太适合搞很重的slab,但是我们可以仿照类似的设计,对于一些比较高端的芯片设计类似的简化机制的内存管理

简单的说,我们定义若干 size-class(例如 16B、32B、64B、128B...),而每个 size-class 管理若干个 slab。Slab 是一整块内存,被划分为 N 个对象槽(slot),Slab 自己可以有三种状态:empty(全空)、partial(部分使用)、full(无空闲)。这个时候,我们分配时从 partial slab 或 empty slab 中拿 slot;释放时加入回 slab 的 free list。

看起来好像没有什么。但是,我们现在可以做临近的合并措施,不至于内存东一片西一片;而且可以快速的匹配最近的大小,当然,因为都已经池化处理了,我们就可以为不同对象类型做专门优化(构造/析构缓存、debug header)。

精简版的

为了篇幅,我们做一个精简版 slab:

  • 静态定义 size-class(运行时选择合适 bucket)
  • 每个 slab 用一段连续内存和一个位图/链表管理空闲槽

简化的关键结构

struct Slab {
    uint8_t* data; // 指向对象存储区
    uint32_t freeBitmap; // 仅示例,最多32个 slot
    Slab* next;
};

struct SlabBucket {
    size_t objSize;
    Slab* partial;
    Slab* full;
    Slab* empty;
};

真实系统会需要更复杂的位图、锁策略和扩展机制,但这个示例足以用于嵌入式场景。


Arena(Region / Bump Allocator)

Arena 常见于:解析器、一次性分配任务、初始化阶段或短生命周期对象池。它的核心简单到令人发笑:

  • 拿一大块内存(或者多个 chunk)
  • 用一个指针 head 记录当前分配位置
  • alloc(size) 就是把 head 向前移动 size,返回旧的位置
  • reset()head 回退到初始位置(一次性回收所有分配)

所以,Arena的分配速度极快(指针运算),但是又非常适合临时内存,零/低碎片。但是问题也很多:

  • 不能单独释放单个对象(除非做更复杂的回收策略)
  • 外部生命周期控制由用户负责
class Arena {
public:
    Arena(void* buffer, size_t size) : base_(reinterpret_cast<uint8_t*>(buffer)), cap_(size), head_(0) {}
    void* alloc(size_t n, size_t align = alignof(std::max_align_t)) {
        size_t cur = reinterpret_cast<size_t>(base_) + head_;
        size_t aligned = (cur + (align - 1)) & ~(align - 1);
        size_t offset = aligned - reinterpret_cast<size_t>(base_);
        if (offset + n > cap_) return nullptr;
        head_ = offset + n;
        return base_ + offset;
    }
    void reset() { head_ = 0; }
private:
    uint8_t* base_;
    size_t cap_;
    size_t head_;
};

当然上面的代码不是线程安全的,这个需要注意了。