RPython GC 对象分配速度大揭秘(废土种田,分配的对象超给力)
off999 2025-06-23 21:20 3 浏览 0 评论
最近,对 RPython GC 的对象分配速度产生了浓厚的兴趣。于是编写了一个小型的 RPython 基准测试程序,试图探究它对象分配的大致速度。
初步测试与问题发现
最初的设想是通过一个紧密循环来分配实例:
class A(object):
pass
def run(loops):
# 初步想法,见下文
for i in range(loops):
a = A()
a.i = i
RPython 类型推断会识别出 A 类实例包含一个整数类型的 i 字段,加上每个 RPython 对象都需要一个用于 GC 元信息的字段,所以在 64 位架构下,A 类实例需要 16 字节内存空间。
然而,这样的测量方式存在缺陷,因为 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) # 在末尾打印实例,防止优化器移除分配
经检查 RPython 编译器生成的 C 代码,确认分配操作未被移除。
同时,我们也可以选择不初始化字段,编写如下代码:
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 支架代码后,使用 pypy rpython/bin/rpython targetallocatealot.py 构建二进制文件,该文件既包含上述代码,也包含 RPython 垃圾回收器。
在 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 对比结果:
$ 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 行为探究
使用 perf 工具获取执行过程中的统计信息:
$ perf stat -e
cache-references,cache-misses,cycles,instructions,branches,faults,migrations
./targetallocatealot-c 10000000000 0 without initialization
<a object at 0x7aa260e35980>
<a object at 0x7aa260e35990>
4.301442 s 149.011612 GB 34.642245 GB/s Performance counter stats for
'./targetallocatealot-c 10000000000 0': 7,244,117,828 cache-references
23,446,661 cache-misses # 0.32% of all cache refs 21,074,240,395 cycles
110,116,790,943 instructions # 5.23 insn per cycle 20,024,347,488 branches
1,287 faults 24 migrations 4.303071693 seconds time elapsed 4.297557000
seconds user 0.003998000 seconds sys $ perf stat -e
cache-references,cache-misses,cycles,instructions,branches,faults,migrations
./targetallocatealot-c 10000000000 1 with initialization
<a object at 0x77ceb0235980>
<a object at 0x77ceb0235990>
5.016772 s 149.011612 GB 29.702688 GB/s Performance counter stats for
'./targetallocatealot-c 10000000000 1': 7,571,461,470 cache-references
241,915,266 cache-misses # 3.20% of all cache refs 24,503,497,532 cycles
130,126,387,460 instructions # 5.31 insn per cycle 20,026,280,693
branches 1,285 faults 21 migrations 5.019444749 seconds time elapsed
5.012924000 seconds user 0.005999000 seconds sys</a
></a
></a
></a
>
结果显示,每个分配操作平均需要约 11 条指令和 2.1 个周期(包括循环相关操作)。
探究 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>
nursery 尺寸为 4 MiB。分配 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 设备上运行相同代码,性能表现明显逊色,进一步证明了硬件进步对程序性能的显著影响。
相关推荐
- 编写更多 pythonic 代码(十三)——Python类型检查
-
一、概述在本文中,您将了解Python类型检查。传统上,类型由Python解释器以灵活但隐式的方式处理。最新版本的Python允许您指定显式类型提示,这些提示可由不同的工具使用,以帮助您更...
- [827]ScalersTalk成长会Python小组第11周学习笔记
-
Scalers点评:在2015年,ScalersTalk成长会完成Python小组完成了《Python核心编程》第1轮的学习。到2016年,我们开始第二轮的学习,并且将重点放在章节的习题上。Pytho...
- 用 Python 画一颗会跳动的爱心:代码里的浪漫仪式感
-
在编程的世界里,代码不仅是逻辑的组合,也能成为表达情感的载体。今天我们就来聊聊如何用Python绘制一颗「会跳动的爱心」,让技术宅也能用代码传递浪漫。无论是写给爱人、朋友,还是单纯记录编程乐趣,这...
- Python面向对象编程(OOP)实践教程
-
一、OOP理论基础1.面向对象编程概述面向对象编程(Object-OrientedProgramming,OOP)是一种编程范式,它使用"对象"来设计应用程序和软件。OOP的核心...
- 如何在 Python 中制作 GIF(python做gif)
-
在数据分析中使用GIF并发现其严肃的一面照片由GregRakozy在Unsplash上拍摄感谢社交媒体,您可能已经对GIF非常熟悉。在短短的几帧中,他们传达了非常具体的反应,只有图片才能传达...
- Python用内置模块来构建REST服务、RPC服务
-
1写在前面和小伙伴们分享一些Python网络编程的一些笔记,博文为《PythonCookbook》读书后笔记整理博文涉及内容包括:TCP/UDP服务构建不使用框架创建一个REST风格的HTTP...
- 第七章:Python面向对象编程(python面向对象六大原则)
-
7.1类与对象基础7.1.1理论知识面向对象编程(OOP)是一种编程范式,它将数据(属性)和操作数据的函数(方法)封装在一起,形成一个称为类(Class)的结构。类是对象(Object)的蓝图,对...
- 30天学会Python编程:8. Python面向对象编程
-
8.1OOP基础概念8.1.1面向对象三大特性8.1.2类与对象关系核心概念:类(Class):对象的蓝图/模板对象(Object):类的具体实例属性(Attribute):对象的状态/数据方法...
- RPython GC 对象分配速度大揭秘(废土种田,分配的对象超给力)
-
最近,对RPythonGC的对象分配速度产生了浓厚的兴趣。于是编写了一个小型的RPython基准测试程序,试图探究它对象分配的大致速度。初步测试与问题发现最初的设想是通过一个紧密循环来分配实...
- 30天学会Python编程:2. Python基础语法结构
-
2.1代码结构与缩进规则定义与原理Python使用缩进作为代码块的分界符,这是Python最显著的特征之一。不同于其他语言使用大括号{},Python强制使用缩进来表示代码层次结构。特性与规范缩进量...
- Python 类和方法(python类的方法与普通的方法)
-
Python类和方法Python类创建、属性和方法具体是如何体现的,代码中如何设计,请继续看下去。蟒蛇类解释在Python中使用OOP?什么是Python类?Python类创建Pyt...
- 动态类型是如何一步步拖慢你的python程序的
-
杂谈人人都知道python慢,这都变成了人尽皆知的事情了,但你知道具体是什么拖慢了python的运行吗?动态类型肯定要算一个!动态类型,能够提高开发效率,能够让我们更加专注逻辑开发,使得编程更加灵活。...
- 用Python让图表动起来,居然这么简单
-
我好像看到这个emoji:动起来了!编译:佑铭参考:https://towardsdatascience.com/how-to-create-animated-graphs-in-python-bb6...
- Python类型提示工程实践:提升代码质量的静态验证方案
-
根据GitHub年度开发者调查报告,采用类型提示的Python项目维护成本降低42%,代码审查效率提升35%。本文通过9个生产案例,解析类型系统在工程实践中的应用,覆盖API设计、数据校验、IDE辅助...
- Python:深度剖析实例方法、类方法和静态方法的区别
-
在Python中,类方法(classmethod)、实例方法(instancemethod)和静态方法(staticmethod)是三种不同类型的函数,它们在使用方式和功能上有一些重要的区别。理...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 编写更多 pythonic 代码(十三)——Python类型检查
- [827]ScalersTalk成长会Python小组第11周学习笔记
- 用 Python 画一颗会跳动的爱心:代码里的浪漫仪式感
- Python面向对象编程(OOP)实践教程
- 如何在 Python 中制作 GIF(python做gif)
- Python用内置模块来构建REST服务、RPC服务
- 第七章:Python面向对象编程(python面向对象六大原则)
- 30天学会Python编程:8. Python面向对象编程
- RPython GC 对象分配速度大揭秘(废土种田,分配的对象超给力)
- 30天学会Python编程:2. Python基础语法结构
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python自定义函数 (53)
- python进度条 (67)
- python吧 (67)
- python字典遍历 (54)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python串口编程 (60)
- python读取文件夹下所有文件 (59)
- java调用python脚本 (56)
- python操作mysql数据库 (66)
- python字典增加键值对 (53)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python人脸识别 (54)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)