RPython GC 对象分配性能深度解析

在软件开发领域,垃圾回收(Garbage Collection,GC)机制的性能对程序的整体表现有着举足轻重的影响。RPython 作为一种专为 PyPy 项目设计的限制性 Python 变体,其内置的 GC 组件在对象分配速度与内存管理效率方面表现卓越。

一、RPython GC 对象分配测试环境搭建

(一)测试代码编写

为探究 RPython GC 的对象分配速度,我们首先编写了一段基础测试代码:

class A(object):
    pass

def run(loops):
    # 初步测试代码
    for i in range(loops):
        a = A()
        a.i = i

然而,初步测试表明,RPython 的静态优化器可能将未被使用的对象分配操作移除。为此,我们对代码进行了优化,确保每次循环都有两个实例处于存活状态:

class A(object):
    pass

def run(loops):
    a = prev = None
    for i in range(loops):
        prev = a
        a = A()
        a.i = i
    print(prev, a)  # 防止优化器移除分配操作

同时,我们还提供了不初始化字段的版本:

def run(initialize_field, loops):
    t1 = time.time()
    if initialize_field:
        a = prev = None
        for i in range(loops):
            prev = a
            a = A()
            a.i = i
        print(prev, a)
    else:
        a = prev = None
        for i in range(loops):
            prev = a
            a = A()
        print(prev, a)
    t2 = time.time()
    print(t2 - t1, 's')
    object_size_in_words = 2  # GC 头信息和一个整数字段
    mem = loops * 8 * object_size_in_words / 1024.0 / 1024.0 / 1024.0
    print(mem, 'GB')
    print(mem / (t2 - t1), 'GB/s')

(二)RPython 支架代码与编译

添加必要的 RPython 支架代码后,通过以下命令将代码编译为二进制文件:

pypy rpython/bin/rpython targetallocatealot.py

该命令将 RPython 代码转换为 C 代码,并进一步编译为包含我们测试代码和 RPython 垃圾回收器的二进制文件。

二、测试结果与性能分析

(一)RPython GC 测试结果

在 AMD Ryzen 7 PRO 7840U(运行 Ubuntu Linux 24.04.2)平台上,测试结果如下:

$ ./targetallocatealot-c 1000000000 0 without initialization
<a object at 0x7c71ad84cf60>
  <a object at 0x7c71ad84cf70>
    0.433825 s 14.901161 GB 34.348322 GB/s $ ./targetallocatealot-c 1000000000 1
    with initialization
    <a object at 0x71b41c82cf60>
      <a object at 0x71b41c82cf70> 0.501856 s 14.901161 GB 29.692100 GB/s</a></a
    ></a
  ></a
>

(二)与 Boehm GC 的对比

为对比效果,我们还测试了 Boehm GC 的性能:

$ pypy rpython/bin/rpython --gc=boehm --output=targetallocatealot-c-boehm
targetallocatealot.py ... $ ./targetallocatealot-c-boehm 1000000000 0 without
initialization
<a object at 0xffff8bd058a6e3af>
  <a object at 0xffff8bd058a6e3bf>
    9.722585 s 14.901161 GB 1.532634 GB/s $ ./targetallocatealot-c-boehm
    1000000000 1 with initialization
    <a object at 0xffff88e1132983af>
      <a object at 0xffff88e1132983bf>
        9.684149 s 14.901161 GB 1.538717 GB/s</a
      ></a
    ></a
  ></a
>

需要注意的是,Boehm GC 使用保守的栈扫描方式,无法移动对象,这使得其分配过程相对复杂。

(三)性能剖析与 GC 行为探究

1. 性能剖析

使用 perf 工具对执行过程进行性能剖析,得到以下关键指标:

  • 每个分配操作平均需要约 11 条指令和 2.1 个周期(包括循环相关操作)
  • 指令与周期比(IPC)达到 5.23,表明 CPU 利用率较高

2. GC 行为探究

RPython GC 根据 L2 缓存大小确定 nursery 尺寸。通过 PYPYLOG 环境变量查看相关信息:

$ PYPYLOG=gc-set-nursery-size,gc-hardware:- ./targetallocatealot-c 1 1
[f3e6970465723] {gc-set-nursery-size nursery size: 270336 [f3e69704758f3]
gc-set-nursery-size} [f3e697047b9a1] {gc-hardware L2cache = 1048576
[f3e69705ced19] gc-hardware} [f3e69705d11b5] {gc-hardware memtotal =
32274210816.000000 [f3e69705f4948] gc-hardware} [f3e6970615f78]
{gc-set-nursery-size nursery size: 4194304 [f3e697061ecc0] gc-set-nursery-size}
with initialization NULL
<a object at 0x7fa7b1434020> 0.000008 s 0.000000 GB 0.001894 GB/s</a>

在分配 14.9 GiB 数据时,GC 需执行约 38146 次 minor 收集。通过日志验证:

$ PYPYLOG=gc-minor:out ./targetallocatealot-c 10000000000 1 with initialization
w<a object at 0x7991e3835980>
  <a object at 0x7991e3835990>
    5.315511 s 149.011612 GB 28.033356 GB/s $ head out [f3ee482f4cd97] {gc-minor
    [f3ee482f53874] {gc-minor-walkroots [f3ee482f54117] gc-minor-walkroots}
    minor collect, total memory used: 0 number of pinned objects: 0 total size
    of surviving objects: 0 time taken: 0.000029 [f3ee482f67b7e] gc-minor}
    [f3ee4838097c5] {gc-minor [f3ee48380c945] {gc-minor-walkroots $ grep
    "{gc-minor-walkroots" out | wc -l 38147</a
  ></a
>

minor 收集耗时统计显示,GC 所占时间约为 2%。

三、机器码层面的探究

RPython GC 的分配快速路径采用简单的指针碰撞(bump pointer)方式,伪代码如下:

result = gc.nursery_free # 将 nursery_free 指针向前移动 totalsize
gc.nursery_free = result + totalsize # 检查此次分配是否会超出 nursery
if gc.nursery_free > gc.nursery_top: # 如果超出,则收集 nursery 并分配
result = collect_and_reserve(totalsize) result.hdr = <A  GC 标志和类型 id></GC>

通过分析编译后的二进制文件 targetallocatealot-c 的机器码,大致定位到核心循环的机器码实现,并尝试注释关键操作:

... cb68: mov %rbx,%rdi cb6b: mov %rdx,%rbx # 初始化上一次迭代分配对象的对象头 cb6e: movq $0x4c8,(%rbx) # 循环终止检查 cb75: cmp %rbp,%r12 cb78: je ccb8 # 加载 nursery_free cb7e: mov 0x33c13(%rip),%rdx # 增加循环计数器 cb85: add $0x1,%rbp # 将 nursery_free 增加 16(对象大小) cb89: lea 0x10(%rdx),%rax # 比较 nursery_top 与新的 nursery_free cb8d: cmp %rax,0x33c24(%rip) # 存储新的 nursery_free cb94: mov %rax,0x33bfd(%rip) # 如果新的 nursery_free 超出 nursery_top,则跳转到慢速路径,否则跳转到顶部 cb9b: jae cb68 # 从这里开始是慢速路径: # 将上次迭代存活对象保存到 GC 影子栈 cb9d: mov %rbx,-0x8(%rcx) cba1: mov %r13,%rdi cba4: mov $0x10,%esi # 执行 minor 收集 cba9: call 20800 <pypy_g_IncrementalMiniMarkGC_collect_and_reserve>
  ...</pypy_g_IncrementalMiniMarkGC_collect_and_reserve
>

四、在常规 Python 环境中的表现

将相同代码作为常规 Python3 程序运行在 PyPy 上。由于动态类型特性,PyPy 上用户自定义类的实例占用空间更大(至少 7 个字,即 56 字节)。但可以改用整数对象进行测试,整数对象在堆上分配,包含两个字(一个用于 GC,一个存储机器字大小的整数值)。

测试代码如下:

import sys, time


def run(loops):
    t1 = time.time()
    a = prev = None
    for i in range(loops):
        prev = a
        a = i
    print(prev, a)  # 确保始终有两个对象存活
    t2 = time.time()
    object_size_in_words = 2  # GC 头信息和一个整数字段
    mem = loops * 28 / 1024.0 / 1024.0 / 1024.0
    print(mem, 'GB')
    print(mem / (t2 - t1), 'GB/s')

def main(argv):
    loops = int(argv[1])
    run(loops)
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv))

运行结果对比(启用与禁用 JIT):

$ pypy3 allocatealot.py 1000000000
999999998 999999999
14.901161193847656 GB
17.857494904899553 GB/s
$ pypy3 --jit off allocatealot.py 1000000000
999999998 999999999
14.901161193847656 GB
0.8275382375297171 GB/s

分析 PyPy JIT 生成的机器码较为复杂,需通过 PYPYLOG=jit:out 启用 JIT 日志记录,找到循环的跟踪 IR,并利用 PyPy 仓库中的脚本进行反汇编分析。

五、总结与展望

(一)总结

RPython GC 的分配快速路径设计精巧,能够实现较高的分配速率。这一设计并非创新,而是垃圾回收器设计领域的常见手法。除了算法本身,现代 CPU 架构的不断进步也为性能提升做出了巨大贡献。在同事的旧款 AMD 设备上运行相同代码,性能表现明显逊色,进一步证明了硬件进步对程序性能的显著影响。

(二)展望

随着软件系统的日益复杂和数据量的不断膨胀,内存管理技术面临着前所未有的挑战。未来,我们可以从以下几个方向进一步探索和优化内存管理策略:

  1. 「自适应内存管理」:开发能够根据应用程序运行时特征动态调整策略的内存管理器,以更好地应对多样化的工作负载。

  2. 「硬件协同设计」:与硬件厂商紧密合作,设计与新型处理器架构(如异构计算平台)相匹配的内存管理技术,充分利用硬件特性来提升内存访问效率。

  3. 「智能化垃圾回收」:借助机器学习技术,使垃圾回收器能够更精准地预测对象生命周期,优化回收时机和策略,减少对程序运行的干扰。

  4. 「分布式系统内存管理」:在分布式计算环境中,研究高效的分布式内存管理方案,降低数据传输开销,提高系统整体性能。

内存管理技术的发展对于提升软件性能、降低硬件成本以及推动整个信息技术产业的进步具有重要意义。我们期待在不久的将来,能够看到更加高效、智能的内存管理解决方案问世,为解决复杂系统中的内存瓶颈问题提供有力支持。