Cinder 是 Meta 内部面向性能的 CPython 3.10 生产版本。它包含许多性能优化,包括字节码内联缓存、协程的即时评估、一次方法 JIT 以及一个实验性字节码编译器,该编译器使用类型注释来发出在 JIT 中性能更好的类型专用字节码。
Cinder 为 Instagram 提供支持,并越来越多地在 Meta 中越来越多的 Python 应用程序中使用。
有关 CPython 的更多信息,请参阅README.cpython.rst
。
简短的回答:不。
我们公开 Cinder 是为了促进有关可能将部分工作上游到 CPython 的讨论,并减少致力于 CPython 性能的人员之间的重复工作。
Cinder 并未经过完善或记录以供其他人使用。我们不希望它成为 CPython 的替代品。我们提供此代码的目标是统一更快的 CPython。因此,虽然我们确实在生产环境中运行 Cinder,但如果您选择这样做,您就得靠自己了。我们无法承诺修复外部错误报告或审查拉取请求。我们确保 Cinder 对于我们的生产工作负载来说足够稳定和快速,但我们不保证其对于任何外部工作负载或用例的稳定性、正确性或性能。
也就是说,如果您有动态语言运行时的经验并且有想法让 Cinder 更快;或者,如果您从事 CPython 工作并希望使用 Cinder 作为改进 CPython 的灵感(或帮助 Cinder 的上游部分迁移到 CPython),请联系我们;我们很乐意聊天!
Cinder 应该像 CPython 一样构建; configure
并make -j
。然而,由于 Cinder 的大多数开发和使用都发生在 Meta 的高度特定上下文中,因此我们不会在其他环境中进行太多练习。因此,构建和运行 Cinder 最可靠的方法是重用 GitHub CI 工作流程中基于 Docker 的设置。
如果您只是想获得一个可以工作的 Cinder 而不想自己构建它,我们的运行时 Docker 镜像将是最简单的(不需要存储库克隆!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
如果您想自己构建:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
请注意,Cinder 仅在 Linux x64 上构建或测试;其他任何东西(包括 macOS)可能都不起作用。上面的 Docker 镜像基于 Fedora Linux,并根据 Cinder 存储库中的 Docker 规范文件构建: .github/workflows/python-build-env/Dockerfile
。
有一些可能有趣的新测试目标。 make testcinder
与make test
几乎相同,只是它跳过了一些在我们的开发环境中存在问题的测试。 make testcinder_jit
在完全启用 JIT 的情况下运行测试套件,因此所有函数都是 JIT 的。 make testruntime
为 JIT 运行一套 C++ gtest 单元测试。并使make test_strict_module
运行严格模块的测试套件(见下文)。
请注意,这些步骤生成的 Cinder Python 二进制文件未启用 PGO/LTO 优化,因此不要指望使用这些指令来对任何 Python 工作负载进行任何加速。
Cinder Explorer 是一个实时游乐场,您可以在其中看到 Cinder 如何将 Python 代码从源代码编译到程序集——欢迎您尝试一下!请随意提交功能请求和错误报告。请记住,Cinder Explorer 与其他部分一样,会尽最大努力“提供支持”。
Instagram 使用多进程网络服务器架构;父进程启动,执行初始化工作(例如加载代码),并分叉数十个工作进程来处理客户端请求。由于多种原因(例如内存泄漏、代码部署),工作进程会定期重新启动,并且其生命周期相对较短。在此模型中,当修改对象的引用计数时,操作系统必须复制包含在父进程中分配的对象的整个页面。实际上,父进程中分配的对象比工作进程的寿命长。所有与引用计数相关的工作都是不必要的。
Instagram 拥有非常庞大的 Python 代码库,并且由于引用计数长寿命对象的写时复制而产生的开销非常大。我们开发了一种名为“不朽实例”的解决方案,以提供一种从引用计数中选择退出对象的方法。有关详细信息,请参阅 Include/object.h。此功能通过定义 Py_IMMORTAL_INSTANCES 进行控制,并且在 Cinder 中默认启用。这对我们在生产中来说是一个巨大的胜利(~5%),但它使直线代码变慢。引用计数操作频繁发生,启用此功能时必须检查对象是否参与引用计数。
“影子代码”或“影子字节码”是我们的专用解释器的实现。它观察通用 Python 操作码执行中的特定可优化情况,并(对于热函数)动态地将这些操作码替换为专用版本。 Shadowcode 的核心位于Shadowcode/shadowcode.c
中,尽管专用字节码的实现与 eval 循环的其余部分一起位于Python/ceval.c
中。 Shadowcode 特定的测试位于Lib/test/test_shadowcode.py
中。
它在本质上与 CPython 3.11 中内置的专用自适应解释器 (PEP-659) 类似。
Instagram 服务器是一个异步繁重的工作负载,其中每个 Web 请求可能会触发数十万个异步任务,其中许多任务可以在不暂停的情况下完成(例如,借助记忆值)。
我们扩展了向量调用协议以传递一个新标志Ci_Py_AWAITED_CALL_MARKER
,指示调用者正在立即等待此调用。
当与立即等待的异步函数调用一起使用时,我们可以立即(热切地)评估被调用的函数,直到完成或第一次挂起。如果函数在没有挂起的情况下完成,我们就可以立即返回值,而无需额外的堆分配。
当与异步收集一起使用时,我们可以立即(热切地)评估传递的可等待集合,从而可能避免为可以同步完成的协程、已完成的 future、记忆值等创建和调度多个任务的成本。
这些优化导致 CPU 效率显着提高 (~5%)。
这主要在Python/ceval.c
中通过新的向量调用标志Ci_Py_AWAITED_CALL_MARKER
实现,指示调用者正在立即等待此调用。查找IS_AWAITED()
宏和此向量调用标志的使用。
Cinder JIT 是用 C++ 实现的一次方法自定义 JIT。它是通过-X jit
标志或PYTHONJIT=1
环境变量启用的。它支持几乎所有Python操作码,并且可以在许多Python性能基准测试中实现1.5-4倍的速度提升。
默认情况下,启用后,它将对曾经调用的每个函数进行 JIT 编译,由于 JIT 编译很少调用的函数的开销,这很可能会使您的程序变慢,而不是更快。选项-X jit-list-file=/path/to/jitlist.txt
或PYTHONJITLISTFILE=/path/to/jitlist.txt
可以将其指向包含完全限定函数名称的文本文件(格式为path.to.module:funcname
或path.to.module:ClassName.method_name
),每行一个,应该是 JIT 编译的。我们使用此选项仅编译一组从生产分析数据派生的热函数。 (JIT 的一种更典型的方法是动态编译函数,因为我们观察到它们被频繁调用。对于我们来说,实现这一点还不值得,因为我们的生产架构是一个预分叉的网络服务器,并且对于由于内存共享原因,我们希望在工作进程分叉之前在初始进程中预先进行所有 JIT 编译,这意味着在决定要 JIT 编译哪些函数之前我们无法观察进程中的工作负载。)
JIT 位于Jit/
目录中,其 C++ 测试位于RuntimeTests/
中(使用make testruntime
运行它们)。 Lib/test/test_cinderjit.py
中也有一些 Python 测试;这些并不意味着详尽无遗,因为我们通过make testcinder_jit
在 JIT 下运行整个 CPython 测试套件;它们涵盖了 CPython 测试套件中未发现的 JIT 边缘情况。
有关影响 JIT 行为的其他一些-X
选项和环境变量,请参阅Jit/pyjit.cpp
。该文件中还定义了一个cinderjit
模块,它将一些 JIT 实用程序公开给 Python 代码(例如,强制编译特定函数、检查函数是否已编译、禁用 JIT)。请注意, cinderjit.disable()
仅禁用将来的编译;它立即编译所有已知函数并保留现有的 JIT 编译函数。
JIT 首先将 Python 字节码降低为高级中间表示(HIR);这是在Jit/hir/
中实现的。 HIR 与 Python 字节码的映射相当接近,尽管它是寄存器机而不是堆栈机,但它的级别要低一些,它是类型化的,并且一些被 Python 字节码掩盖但对性能很重要的细节(尤其是引用计数)是在 HIR 中明确暴露。 HIR被转换为SSA形式,对其执行一些优化,然后根据有关HIR操作码的引用计数和内存影响的元数据自动将引用计数操作插入其中。
然后,HIR 被降低为低级中间表示 (LIR),这是对汇编的抽象,在Jit/lir/
中实现。在 LIR 中,我们进行寄存器分配、一些额外的优化,最后使用优秀的 asmjit 库将 LIR 降级为汇编(在Jit/codegen/
中)。
JIT 尚处于早期阶段。虽然它已经可以消除解释器循环开销并为许多函数提供显着的性能改进,但我们才刚刚开始触及可能优化的表面。许多常见的编译器优化尚未实现。我们的优化优先顺序很大程度上是由 Instagram 制作工作负载的特征决定的。
严格的模块是将一些东西合二为一:
1. 静态分析器能够验证执行模块的顶级代码不会在该模块外部产生可见的副作用。
2. 不可变的StrictModule
类型可用于代替 Python 的默认模块类型。
3. 一个 Python 模块加载器,能够识别选择严格模式的模块(通过模块顶部的import __strict__
),分析它们以验证没有导入副作用,并将它们作为StrictModule
对象填充到sys.modules
中。
静态 Python 是一种字节码编译器,它利用类型注释来生成类型专用且经过类型检查的 Python 字节码。与 Cinder JIT 一起使用,它在许多情况下可以提供类似于 MyPyC 或 Cython 的性能,同时提供纯 Python 开发人员体验(正常的 Python 语法,无需额外的编译步骤)。静态 Python 加上 Cinder JIT 在 Richards 基准测试的类型化版本上实现了普通 CPython 18 倍的性能。在 Instagram,我们已成功在生产中使用静态 Python 来替换我们主要网络服务器代码库中的所有 Cython 模块,并且没有出现性能下降。
静态Python编译器构建在Python compiler
模块之上,该模块在Python 3中从标准库中删除,此后一直在外部进行维护和更新;该编译器已合并到Lib/compiler
中的 Cinder 中。静态Python编译器在Lib/compiler/static/
中实现,其测试在Lib/test/test_compiler/test_static.py
中。
静态 Python 模块中定义的类会自动给出类型化槽(基于对其类型化类属性的检查和__init__
中带注释的赋值),并且针对这些类型的实例的属性加载和存储使用新的STORE_FIELD
和LOAD_FIELD
操作码,这些操作码在 JIT 中变得直接从对象中的固定内存偏移量加载/存储到对象中,没有LOAD_ATTR
或STORE_ATTR
的间接寻址。类还获得其方法的虚函数表,供下面提到的INVOKE_*
操作码使用。这些功能的运行时支持位于StaticPython/classloader.h
和StaticPython/classloader.c
中。
静态 Python 函数以隐藏的序言开头,它检查提供的参数类型是否与类型注释匹配,如果不匹配则引发TypeError
。从静态 Python 函数到另一个静态 Python 函数的调用将跳过此操作码(因为编译器已验证类型)。静态到静态调用还可以避免典型 Python 函数调用的大部分开销。我们发出一个INVOKE_FUNCTION
或INVOKE_METHOD
操作码,其中携带有关被调用函数或方法的元数据;加上可选的不可变模块(通过StrictModule
)和类型(通过cinder.freeze_type()
,我们目前将其应用于导入加载器中严格和静态模块中的所有类型,但将来可能会成为静态 Python 的固有部分)并编译被调用者签名的实时知识使我们能够(在 JIT 中)将许多 Python 函数调用转换为使用 x64 调用约定对固定内存地址的直接调用,而开销只比 C 函数调用多一点。
静态Python仍然是逐渐类型化的,并且通过回退到正常的Python动态行为来支持仅部分注释或使用未知类型的代码。在某些情况下(例如,当从带有返回注释的函数返回静态未知类型的值时),会插入运行时CAST
操作码,如果运行时类型与预期类型不匹配,则会引发TypeError
。
静态 Python 还支持机器整数、布尔值、双精度数和向量/数组的新类型。在 JIT 中,这些值被处理为未装箱的值,例如原始整数算术避免了所有 Python 开销。对内置类型(例如列表或字典下标或len()
)的一些操作也进行了优化。
Cinder 通过严格/静态模块加载器支持逐步采用静态模块,该加载器可以自动检测静态模块并通过跨模块编译将其加载为静态模块。加载器将在文件顶部查找import __static__
和import __strict__
注释,并适当地编译模块。要启用加载程序,您可以选择以下三个选项之一:
1. 通过from cinderx.compiler.strict.loader import install; install()
。
PYTHONINSTALLSTRICTLOADER=1
。./python -X install-strict-loader application.py
。或者,您可以使用./python -m compiler --static some_module.py
静态编译所有代码,这会将模块编译为静态 Python 并执行它。
有关更详细的文档,请参阅CinderDoc/static_python.rst
。