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 设备上运行相同代码,性能表现明显逊色,进一步证明了硬件进步对程序性能的显著影响。
(二)展望
随着软件系统的日益复杂和数据量的不断膨胀,内存管理技术面临着前所未有的挑战。未来,我们可以从以下几个方向进一步探索和优化内存管理策略:
-
「自适应内存管理」:开发能够根据应用程序运行时特征动态调整策略的内存管理器,以更好地应对多样化的工作负载。
-
「硬件协同设计」:与硬件厂商紧密合作,设计与新型处理器架构(如异构计算平台)相匹配的内存管理技术,充分利用硬件特性来提升内存访问效率。
-
「智能化垃圾回收」:借助机器学习技术,使垃圾回收器能够更精准地预测对象生命周期,优化回收时机和策略,减少对程序运行的干扰。
-
「分布式系统内存管理」:在分布式计算环境中,研究高效的分布式内存管理方案,降低数据传输开销,提高系统整体性能。
内存管理技术的发展对于提升软件性能、降低硬件成本以及推动整个信息技术产业的进步具有重要意义。我们期待在不久的将来,能够看到更加高效、智能的内存管理解决方案问世,为解决复杂系统中的内存瓶颈问题提供有力支持。