Skip to content

为了研究 Python JIT 是如何运行的,我们需要将源码编译成 DEBUG 模式的二进制文件,Python 源码编译需要增加参数 --enable-experimental-jit --with-pydebug --with-trace-refs。依旧使用 VSCode 对源码进行调试。之前有一篇文章介绍 Python 内核源码解析:使用 VS Code 调试 Python 内核代码。需要注意的是,我们使用 docker 编译生成 Python 二进制程序,在目录映射上需要保持一致,也就是 volumes 的映射路径一致。这样,在断点调试的时候,VSCode 就能够找到对应文件的行号。

Python JIT 机器代码的生成其实用不到调试,直接看 Python 的源码就可以知道。

Python JIT 的核心文件是 jit_stencils.h,该文件存储了 Python 指令与机器代码的映射关系。该文件是编译过程中生成的,因为 CPU 的架构不同,机器代码也不同,Python 3.1.4 JIT 支持如下编译工具链:

  • aarch64-apple-darwin/clang
  • aarch64-pc-windows/msvc
  • aarch64-unknown-linux-gnu/clang
  • aarch64-unknown-linux-gnu/gcc
  • i686-pc-windows-msvc/msvc
  • x86_64-apple-darwin/clang
  • x86_64-pc-windows-msvc/msvc
  • x86_64-unknown-linux-gnu/clang
  • x86_64-unknown-linux-gnu/gcc

这里简单的介绍一下 编译工具链三元组格式介绍,格式如:{arch}-{vendor}-{sys}-{abi},明明有四个变量,为什么叫三元组格式呢?因为 vendor 或者 abi 可以省略。

  • 架构(architecture):arm。
  • 供应商(vendor):unknown。这里 unknown 就是未指定供应商或者是不重要。
  • 系统(system):linux。
  • ABI:gnueabihf。gnueabihf 表示系统使用 glibc 作为其 C 标准库 libc 实现,并且具有硬件加速浮点算法。

Python JIT 的相关设计文档可以参考 PEP 744 – JIT Compilation。机器代码通过 LLVM 生成,所以编译前需要安装好 Clang-19,之前的文件已经讲解了如何安装 Python-3.14。接下来讲讲编译过程中是如何生成 jit_stencils.h 的。Tools/jit 目录下是生成该文件的脚本,Tools/jit/build.py 是生成的入口文件,target 参数就是编译工具链,也就是上面支持的列表。

首先脚本会读取文件 Python/executor_cases.c.h 的内容(这个文件的内容也是通过其它的脚本生成的,不得不感叹脚本和生成的强大),然后获取所有的 case,对其进行排序。循环这些 case,调用 self._compile(.., .., ..) 进行编译

python
async def _build_stencils(self) -> dict[str, _stencils.StencilGroup]:
    generated_cases = PYTHON_EXECUTOR_CASES_C_H.read_text()
    cases_and_opnames = sorted(
        re.findall(
            r"\n {8}(case (\w+): \{\n.*?\n {8}\})", generated_cases, flags=re.DOTALL
        )
    )
    tasks = []
    with tempfile.TemporaryDirectory() as tempdir:
        work = pathlib.Path(tempdir).resolve()
        async with asyncio.TaskGroup() as group:
            coro = self._compile("shim", TOOLS_JIT / "shim.c", work)
            tasks.append(group.create_task(coro, name="shim"))
            template = TOOLS_JIT_TEMPLATE_C.read_text()
            for case, opname in cases_and_opnames:
                # Write out a copy of the template with *only* this case
                # inserted. This is about twice as fast as #include'ing all
                # of executor_cases.c.h each time we compile (since the C
                # compiler wastes a bunch of time parsing the dead code for
                # all of the other cases):
                c = work / f"{opname}.c"
                c.write_text(template.replace("CASE", case))
                coro = self._compile(opname, c, work)
                tasks.append(group.create_task(coro, name=opname))
    return {task.get_name(): task.result() for task in tasks}

self._compile 代码如下,构建 clang 的参数,调用 clang 命令行工具,生成对应的 .o 文件。self._parse(o) 会调用 llvm-object-dump 获取到相关二进制代码和汇编代码。然后构造一个 _stencils.StencilGroup 的对象,添加到 tasks 数组中。最后遍历这个数组,将所有的机器代码和汇编代码(注释)写入到 jit_stencils.h 中。

python
async def _compile(
    self, opname: str, c: pathlib.Path, tempdir: pathlib.Path
) -> _stencils.StencilGroup:
    o = tempdir / f"{opname}.o"
    args = [
        f"--target={self.triple}",
        "-DPy_BUILD_CORE_MODULE",
        "-D_DEBUG" if self.debug else "-DNDEBUG",
        f"-D_JIT_OPCODE={opname}",
        "-D_PyJIT_ACTIVE",
        "-D_Py_JIT",
        "-I.",
        f"-I{CPYTHON / 'Include'}",
        f"-I{CPYTHON / 'Include' / 'internal'}",
        f"-I{CPYTHON / 'Include' / 'internal' / 'mimalloc'}",
        f"-I{CPYTHON / 'Python'}",
        f"-I{CPYTHON / 'Tools' / 'jit'}",
        "-O3",
        "-c",
        # This debug info isn't necessary, and bloats out the JIT'ed code.
        # We *may* be able to re-enable this, process it, and JIT it for a
        # nicer debugging experience... but that needs a lot more research:
        "-fno-asynchronous-unwind-tables",
        # Don't call built-in functions that we can't find or patch:
        "-fno-builtin",
        # Emit relaxable 64-bit calls/jumps, so we don't have to worry about
        # about emitting in-range trampolines for out-of-range targets.
        # We can probably remove this and emit trampolines in the future:
        "-fno-plt",
        # Don't call stack-smashing canaries that we can't find or patch:
        "-fno-stack-protector",
        "-std=c11",
        "-o",
        f"{o}",
        f"{c}",
        *self.args,
    ]
    await _llvm.run("clang", args, echo=self.verbose)
    return await self._parse(o)

Python 的 JIT 目前仍处于实验性阶段,所有我们没必要投入过多的精力去深入的了解 Python JIT 是如何运作的。要知道,这无非就是一个从“解释并执行”,转变成为“将字节码翻译成机器码,然后由系统执行”的过程。