python虚拟机集锦(4)-字节码解释器 3.11(1)

字节码解释器V3.11概述

CPython 3.11字节码解释器(也称为虚拟机)比3.10有许多改进,描述3.11解释器的内部工作,重点是不仅理解代码,而且理解其设计。虽然解释器永远在进化,3.12设计无疑会再次不同,但了解3.11设计将帮助您了解未来对解释器的改进。

介绍

在Python/ceval.c中,字节码解释器的任务是执行Python代码。它的主要输入是一个代码对象,尽管这不是解释器的直接参数。解释器被构造为一个(递归)函数,采用线程状态(tstate)和堆栈帧(frame)。该函数还接受generator.throw实现使用的整数throwflag。它返回对Python对象(PyObject*)的新引用或错误指示符NULL。根据PEP 523,该功能可通过设置interp->eval_frame进行配置;只描述了默认函数_PyEval_EvalFrameDefault()。(此函数的签名已经演变,不再与PEP523指定的匹配;添加了线程状态参数,堆栈帧参数不再是对象。)

解释器通过查看堆栈帧(frame->f_code)找到代码对象。解释器所需的各种其他项(例如全局项和内置项)也可以通过堆栈框架访问。线程状态存储异常信息和各种其他信息,例如递归深度。线程状态还用于访问每个解释器状态(tstate->interp)和每个运行时(即真正全局)状态(tsstate->interp->runtime)。

注意这里的术语有点混淆。“解释器”是指字节码解释器,一种递归函数。“解释器状态”是指线程共享的状态,每个线程都可能运行自己的字节码解释器。单个进程甚至可以托管多个解释器,每个解释器都有自己的解释器状态,但共享运行时状态。多个政治公众人物(PEP 684、PEP 630和PEP 554)涵盖了多个口译员的话题(还有更多)。当前文档主要关注字节码解释器。

代码对象

解释器使用代码对象(frame->f_code)作为其起点。代码对象包含解释器使用的许多字段,以及一些供调试器和其他工具使用的字段。在3.11中,代码对象的最后一个字段是一个长度不确定的数组,包含字节码code->cocode_adaptive。(在以前的版本中,代码对象是一个字节对象,code->cocode;它被更改为保存分配并允许其进行变异。)

代码对象通常由字节码编译器生成,尽管它们通常由一个进程写入磁盘并由另一个进程读入。代码对象的磁盘版本是使用封送协议序列化的。一些代码对象使用Tools/scripts/deepfreet.py预加载到解释器中,该工具编写Python/deepfreet/deepfreed.c。

代码对象名义上是不可变的。某些字段(包括cocode_adaptive)是可变的,但当对代码对象进行散列或比较时,不包括可变字段。

引用PEP部分

PEP 523

向CPython添加框架评估API
摘要

该PEP建议扩展CPython的C API[2],以允许指定每个解释器函数指针来处理帧的求值[5]。该建议还建议向代码对象[3]添加一个新字段,以存储任意数据,供帧求值函数使用。

根本原因

Python缺乏灵活性的一个地方是直接执行Python代码。虽然CPython的C API[2]允许构建进入帧对象的数据,然后通过PyEval_EvalFrameEx()[5]对其进行评估,但Python代码的执行控制权取决于单个对象,而不是帧级别的整体执行控制。

虽然想要对帧求值产生影响似乎有点太低级了,但它确实为CPython引入方法级JIT之类的东西提供了可能性,而CPython本身不必提供JIT。通过允许外部C代码控制帧求值,JIT可以在求值发生的关键点参与Python代码的执行。然后,这允许JIT根据需要有条件地将Python字节码重新编译为机器代码,同时允许在不需要运行JIT时执行常规CPython字节码。这可以通过允许解释器指定调用什么函数来评估帧来实现。通过将API置于框架评估级别,它可以完整地查看JIT代码的执行环境。

这种指定框架评估函数的能力还允许在JIT之前打开CPython以外的其他用例。例如,使用此API在调用级别实现跟踪或分析功能并不困难。虽然CPython确实提供了在Python级别设置跟踪或分析功能的能力,但这将能够匹配探查器的数据收集,并且通过简单地跳过每行跟踪支持,跟踪速度可能会更快。

它还打开了调试的可能性,其中帧求值函数仅在检测到将要执行特定代码对象时执行特殊的调试工作。在这种情况下,字节码理论上可以重写到位,以便在适当的时候注入断点函数调用,以帮助调试,而不必按照sys.settirace()的要求执行繁重的方法。

为了帮助简化这些用例,我们还建议通过一个新字段在代码对象上添加一个“暂存空间”。这将允许每个代码对象数据与代码对象本身一起存储,以便根据需要由帧评估功能轻松检索。字段本身只是一个PyObject*类型,因此存储在字段中的任何数据都将参与正常的对象内存管理。

以下所有建议的C API更改都不属于稳定ABI的一部分。

展开PyCodeObject

将向PyCodeObject结构中添加一个字段

typedef struct {
    
    
   ...
   void *co_extra;  /* "Scratch space" for the code object. */
} PyCodeObject;

默认情况下,coextra将为NULL,仅在需要时填写。为了使代码对象正常工作,不需要存储在字段中的值,因此可以接受字段数据的丢失。

引入了一个专用API用于该领域:

PyAPI_FUNC(Py_ssize_t) _PyEval_RequestCodeExtraIndex(freefunc);
PyAPI_FUNC(int) _PyCode_GetExtra(PyObject *code, Py_ssize_t index,
                                 void **extra);
PyAPI_FUNC(int) _PyCode_SetExtra(PyObject *code, Py_ssize_t index,
                                 void *extra);

该字段的用户需要调用_PyEval_RequestCodeExtraIndex()来接收(应该考虑的)不透明的索引值,以便将数据添加到co-extra中。使用该索引,用户可以使用_PyCode_SetExtra()设置数据,然后使用_PyCode _GetExtra)检索数据。API被特意列为私有,以传达这样一个事实:Python版本之间没有API的语义保证。

考虑使用列表和元组,但发现其性能较差,关键用例是JIT使用,使用自定义结构而不是Python对象的性能考虑胜出。

PEP 684

概述 每个解释器的GIL

自Python 1.5(1997)以来,CPython用户可以在同一进程中运行多个解释器。然而,同一进程中的解释器始终共享大量的全局状态。这是bug的来源,随着越来越多的人使用该功能,其影响越来越大。
此外,充分的隔离将促进真正的多核并行,在这种情况下,解释器不再共享GIL。
本提案中概述的变化将导致解释器被隔离。
在高层次上,该提案以以下方式改变了CPython:

  • 在充分隔离的情况下,停止在解释器之间共享GIL
  • 为隔离设置添加几个新的解释器配置选项
  • 防止不兼容的扩展导致问题

GIL

GIL保护对大多数CPython运行时状态的并发访问。因此,所有受GIL保护的全局状态必须在GIL能够移动之前移动到每个解释器。(在少数情况下,可以使用其他机制来确保线程安全共享,例如锁或“immortal”对象。)

CPython运行时状态

正确隔离解释器需要将CPython的大部分运行时状态存储在PyInterpreterState结构中。
目前,只有一部分是;其余部分在C全局变量或_PyRuntimeState中找到。其中大部分将不得不搬迁。

这直接与(多年来)正在进行的一项努力相吻合,即大幅减少全局变量的内部使用,并将运行时状态合并为_PyRuntimeState和PyInterpreterState。(见下文巩固运行时全球状态。)该项目本身具有重大价值,几乎没有争议。因此,虽然每个解释器的GIL依赖于完成这项工作,但该项目不应被视为本提案的一部分,而应视为一个依赖项。

其他隔离注意事项

CPython的解释器必须严格隔离,很少有例外。在很大程度上,他们已经做到了。每个解释器都有自己的所有模块、类、函数和变量的副本。CPython C-API文档将作进一步解释。

然而,除了已经提到的(例如GIL),解释器仍有几种方式共享某种状态。

首先,共享一些进程全局资源(例如内存、文件描述符、环境变量)。没有计划改变这一点。

其次,由于错误或实现没有考虑到多个解释器,某些隔离是错误的。这包括CPython的运行时和stdlib,以及依赖全局变量的扩展模块。在这些情况下,应该打开Bug,因为有些已经打开了。

取决于永生不灭的对象(Immortal Objects)

PEP683引入不朽对象作为CPython的内部特性。使用永生不灭对象(Immortal Objects),可以在所有解释器之间共享任何其他不可变的全局对象。因此,该PEP不需要解决如何处理公共C-API中公开的各种对象。它还简化了如何处理内置静态类型的问题。(请参见下面的全局对象。)

这两个问题都有不同的解决方案,但使用不朽的对象,一切都会更简单。如果PEP 683不被接受,则将使用备选方案更新该PEP 683。这使我们能够在这项提案中减少噪音。

动机

我们在这里解决的根本问题是CPython运行时缺乏真正的多核并行性(对于Python代码)。GIL是原因。虽然它在实践中通常不是问题,但至少它使Python的多核故事变得模糊不清,这使得GIL始终分散注意力。

间接收益

每个解释器GIL所需的大部分工作都有好处,无论怎样,这些任务都值得完成:

使多解释器行为更可靠

导致了对长期运行时错误的修复,否则这些错误不会被优先考虑

一直在暴露(并启发修复)以前未知的运行时错误

已驱动清洁器运行时初始化(PEP 432、PEP 587)

推动了更干净、更完整的运行时定稿

导致C-API的结构分层(例如Include/内部)

此外,这些工作中的许多工作对其他CPython相关项目都有好处:

性能改进(“更快的cpython”)

预分叉应用程序部署(例如Instagram服务器)

扩展模块隔离(见PEP 630等)

嵌入CPython

多重解释器的现有使用
用于多个解释器的C-API已使用多年。然而,直到最近,该功能才广为人知,也没有被广泛使用(mod_wsgi除外)。

猜你喜欢

转载自blog.csdn.net/AI_LX/article/details/128738426
今日推荐