Hotdry.
systems-engineering

在 Plush 语言中实现复制式 GC:活对象迁移、压实与根扫描

剖析 Plush 语言复制式垃圾回收机制:活对象从 fromspace 迁移至 tospace、内存压实过程及根扫描策略,实现 actor 间高效无锁内存管理。

在 actor-based 并行编程语言中,实现高效的内存管理是关键挑战。Plush 语言采用 per-actor 的复制式垃圾回收(Copying GC),基于 Cheney 算法,利用半空间(semispace)设计,每个 actor 拥有独立的私有堆和邮箱堆。这种设计避免了全局锁,提高了分配速度和并发性能,同时通过活对象迁移和内存压实,确保堆内存紧凑利用。

复制式 GC 的核心在于将堆分为 fromspace 和 tospace 两个等大空间。垃圾回收触发时,从 fromspace 中的根对象开始扫描,复制所有可达活对象到 tospace,并使用转发指针(forwarding pointer)记录新位置。扫描过程采用深度优先或广度优先遍历根集,包括 actor 的调用栈帧、全局变量(子 actor 启动时复制)和邮箱消息队列。这种根扫描确保不会遗漏引用,同时复制过程天然完成内存压实:活对象被顺序放置在 tospace 开头,形成连续块,避免碎片化。

在 Plush VM 中,分配采用 bump pointer:私有堆使用快速的无锁 bump 分配器,直接从当前堆顶推进指针。新对象只需检查堆剩余空间,速度极快。邮箱堆用于接收消息,发送方锁定接收者邮箱分配器进行复制,确保接收 actor 无需中断执行。证据显示,这种双分配器设计使 ping-pong 消息传递基准在 M1 MacBook 上 500k 次仅需 2 秒,证明了高效性。GC 暂停时间短,例如 Amiga Boing Ball 演示在 60fps 下每秒 GC 一次,用户几乎无感知。

落地实现时,需要关注以下参数和清单:

  1. 堆大小与增长策略

    • 初始私有堆:1MB,邮箱堆:512KB。
    • 增长阈值:占用率达 75% 时翻倍(上限 256MB,避免 OOM)。
    • 公式:new_size = min (current_size * 2, max_heap),使用 Rust 的 Vec 实现 semispace。
  2. GC 触发阈值

    • 私有堆:fromspace 占用 > 50% 时触发(平衡暂停与吞吐)。
    • 邮箱堆:队列长度 > 1000 消息或占用 > 80% 时,发送方 backpressure(重试机制)。
    • 监控:记录 GC 频率(目标 < 1s / 次)、暂停时间(< 10ms)。
  3. 根扫描与对象布局

    • 根集:栈(每个帧的局部变量引用)、globals map、mailbox queue。
    • 对象头:8 字节(标记 + forwarding ptr),支持 64-bit 引用。
    • 复制函数伪码:
      fn copy(obj: *mut Object) -> *mut Object {
          if forwarded(obj) { return forward_ptr(obj); }
          let new = bump_alloc(size(obj));
          copy_fields(obj, new);
          set_forward(obj, new);
          return new;
      }
      
    • 压实收益:活对象率 25% 时,回收 75% 空间。
  4. 优化与监控点

    • 避免大消息:>1KB 对象分拆或共享不可变数据(未来扩展)。
    • 性能瓶颈:大链表 GC 慢(>300K 对象),检查 hashmap 或 quadratic scan,使用 profilers 如 perf。
    • 回滚:若 GC 失败,fallback 到保守标记(但 Plush 用精确 GC)。
    • 指标:分配速率(>1GB/s/actor)、GC CPU 利用(<20%)。

这种机制特别适合 actor 系统:独立 GC 无需 stop-the-world,复制开销被现代 CPU memcpy 速度抵消(10GB/s+)。相比标记 - 清扫,复制式提供零碎片和快速分配,代价是双倍堆空间(可通过 generational 优化)。Plush 证明,即使玩具语言,也能实现生产级内存管理,支持实时 3D 图形。

资料来源:

查看归档