Skip to content

Python 源码目前最新的版本是 3.14.0a2。为了研究一下 Python JIT,不得不自己手动编译下源代码,目标机器是 Linux ydl-ubuntu 6.8.0-48-generic #48~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Oct 7 11:24:13 UTC 2 x86_64 x86_64 x86_64 GNU/Linux。Python 源码编译其实很简单,如果想编译带上 JIT,就需要额外的添加参数,可以通过这个命令 ./configure -h |grep jit 来获取 JIT 相关的参数。

bash
./configure --enable-experimental-jit # 开启实验性的 JIT
make && make install

Python-3.14.0a2 依赖 Python-3.11,以及 clang-19。clang-19 算是比较新的版本,一般的机器上不会安装这个,机器目前版本是 Ubuntu clang version 14.0.0-1ubuntu1.1,要从 clang-14 升级到 clang-19 不是一件容易的事情(这是后话,安装过程遇到了太多的坑,甚至想过要放弃)。

为什么需要安装 clang-19?

其实通过报错信息我们看到,make 过程中需要执行一个命令来生成文件 jit_stencils.h:

bash
/usr/bin/python3.11 ./Tools/jit/build.py x86_64-pc-linux-gnu

这里我想提前揭秘 jit_stencils.h 是做什么的,编译成功之后文件的内容包含如下代码。你会发现其实就是 JIT 指令和机器码的映射表。这里我就不一一列举了,机器代码和机器相关,所以需要 llvm 来生成当前机器的映射关系。

c

void
emit__BINARY_OP(
    unsigned char *code, unsigned char *data, _PyExecutorObject *executor,
    const _PyUOpInstruction *instruction, jit_state *state)
{
    // 
    // /tmp/tmp_s7bqlsz/_BINARY_OP.o:  file format elf64-x86-64
    // 
    // Disassembly of section .text:
    // 
    // 0000000000000000 <_JIT_ENTRY>:
    // 0: 55                            pushq   %rbp
    // 1: 49 8b 6d f0                   movq    -0x10(%r13), %rbp
    // 5: 49 8b 5d f8                   movq    -0x8(%r13), %rbx
    // 9: 4d 89 6c 24 40                movq    %r13, 0x40(%r12)
    // e: 0f b7 05 00 00 00 00          movzwl  (%rip), %eax            # 0x15 <_JIT_ENTRY+0x15>
    // 0000000000000011:  R_X86_64_GOTPCREL    _JIT_OPARG-0x4
    // 15: 48 8b 0d 00 00 00 00          movq    (%rip), %rcx            # 0x1c <_JIT_ENTRY+0x1c>
    // 0000000000000018:  R_X86_64_REX_GOTPCRELX       _PyEval_BinaryOps-0x4
    // 1c: 48 89 ef                      movq    %rbp, %rdi
    // 1f: 48 89 de                      movq    %rbx, %rsi
    // 22: ff 14 c1                      callq   *(%rcx,%rax,8)
    // 25: 49 89 c7                      movq    %rax, %r15
    // 28: 4d 8b 6c 24 40                movq    0x40(%r12), %r13
    // 2d: 49 c7 44 24 40 00 00 00 00    movq    $0x0, 0x40(%r12)
    // 36: 48 8b 45 00                   movq    (%rbp), %rax
    // 3a: 85 c0                         testl   %eax, %eax
    // 3c: 78 09                         js      0x47 <_JIT_ENTRY+0x47>
    // 3e: 48 ff c8                      decq    %rax
    // 41: 48 89 45 00                   movq    %rax, (%rbp)
    // 45: 74 1a                         je      0x61 <_JIT_ENTRY+0x61>
    // 47: 48 8b 03                      movq    (%rbx), %rax
    // 4a: 85 c0                         testl   %eax, %eax
    // 4c: 78 23                         js      0x71 <_JIT_ENTRY+0x71>
    // 4e: 48 ff c8                      decq    %rax
    // 51: 48 89 03                      movq    %rax, (%rbx)
    // 54: 75 1b                         jne     0x71 <_JIT_ENTRY+0x71>
    // 56: 48 89 df                      movq    %rbx, %rdi
    // 59: ff 15 00 00 00 00             callq   *(%rip)                 # 0x5f <_JIT_ENTRY+0x5f>
    // 000000000000005b:  R_X86_64_GOTPCRELX   _Py_Dealloc-0x4
    // 5f: eb 10                         jmp     0x71 <_JIT_ENTRY+0x71>
    // 61: 48 89 ef                      movq    %rbp, %rdi
    // 64: ff 15 00 00 00 00             callq   *(%rip)                 # 0x6a <_JIT_ENTRY+0x6a>
    // 0000000000000066:  R_X86_64_GOTPCRELX   _Py_Dealloc-0x4
    // 6a: 48 8b 03                      movq    (%rbx), %rax
    // 6d: 85 c0                         testl   %eax, %eax
    // 6f: 79 dd                         jns     0x4e <_JIT_ENTRY+0x4e>
    // 71: 4d 85 ff                      testq   %r15, %r15
    // 74: 74 0f                         je      0x85 <_JIT_ENTRY+0x85>
    // 76: 4d 89 7d f0                   movq    %r15, -0x10(%r13)
    // 7a: 49 83 c5 f8                   addq    $-0x8, %r13
    // 7e: 5d                            popq    %rbp
    // 7f: ff 25 00 00 00 00             jmpq    *(%rip)                 # 0x85 <_JIT_ENTRY+0x85>
    // 0000000000000081:  R_X86_64_GOTPCRELX   _JIT_CONTINUE-0x4
    // 85: 5d                            popq    %rbp
    // 86: ff 25 00 00 00 00             jmpq    *(%rip)                 # 0x8c <_JIT_ENTRY+0x8c>
    // 0000000000000088:  R_X86_64_GOTPCRELX   _JIT_ERROR_TARGET-0x4
    // 8c: 
    const unsigned char code_body[140] = {
        0x55, 0x49, 0x8b, 0x6d, 0xf0, 0x49, 0x8b, 0x5d,
        0xf8, 0x4d, 0x89, 0x6c, 0x24, 0x40, 0x0f, 0xb7,
        0x05, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x0d,
        0x00, 0x00, 0x00, 0x00, 0x48, 0x89, 0xef, 0x48,
        0x89, 0xde, 0xff, 0x14, 0xc1, 0x49, 0x89, 0xc7,
        0x4d, 0x8b, 0x6c, 0x24, 0x40, 0x49, 0xc7, 0x44,
        0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b,
        0x45, 0x00, 0x85, 0xc0, 0x78, 0x09, 0x48, 0xff,
        0xc8, 0x48, 0x89, 0x45, 0x00, 0x74, 0x1a, 0x48,
        0x8b, 0x03, 0x85, 0xc0, 0x78, 0x23, 0x48, 0xff,
        0xc8, 0x48, 0x89, 0x03, 0x75, 0x1b, 0x48, 0x89,
        0xdf, 0xff, 0x15, 0x00, 0x00, 0x00, 0x00, 0xeb,
        0x10, 0x48, 0x89, 0xef, 0xff, 0x15, 0x00, 0x00,
        0x00, 0x00, 0x48, 0x8b, 0x03, 0x85, 0xc0, 0x79,
        0xdd, 0x4d, 0x85, 0xff, 0x74, 0x0f, 0x4d, 0x89,
        0x7d, 0xf0, 0x49, 0x83, 0xc5, 0xf8, 0x5d, 0xff,
        0x25, 0x00, 0x00, 0x00, 0x00, 0x5d, 0xff, 0x25,
        0x00, 0x00, 0x00, 0x00,
    };
    // 0: 
    // 0: OPARG
    // 8: &_PyEval_BinaryOps+0x0
    // 10: &_Py_Dealloc+0x0
    // 18: CONTINUE
    // 20: ERROR_TARGET
    const unsigned char data_body[40] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    };
    memcpy(data, data_body, sizeof(data_body));
    patch_64(data + 0x0, instruction->oparg);
    patch_64(data + 0x8, (uintptr_t)&_PyEval_BinaryOps);
    patch_64(data + 0x10, (uintptr_t)&_Py_Dealloc);
    patch_64(data + 0x18, (uintptr_t)code + sizeof(code_body));
    patch_64(data + 0x20, state->instruction_starts[instruction->error_target]);
    memcpy(code, code_body, sizeof(code_body));
    patch_32r(code + 0x11, (uintptr_t)data + -0x4);
    patch_x86_64_32rx(code + 0x18, (uintptr_t)data + 0x4);
    patch_x86_64_32rx(code + 0x5b, (uintptr_t)data + 0xc);
    patch_x86_64_32rx(code + 0x66, (uintptr_t)data + 0xc);
    patch_x86_64_32rx(code + 0x81, (uintptr_t)data + 0x14);
    patch_x86_64_32rx(code + 0x88, (uintptr_t)data + 0x1c);
}

static const StencilGroup stencil_groups[MAX_UOP_ID + 1] = {
    [_BINARY_OP] = {emit__BINARY_OP, 140, 40, {0}},
    [_BINARY_OP_ADD_FLOAT] = {emit__BINARY_OP_ADD_FLOAT, 52, 24, {0}},
    [_BINARY_OP_ADD_INT] = {emit__BINARY_OP_ADD_INT, 182, 40, {0}},
    [_BINARY_OP_ADD_UNICODE] = {emit__BINARY_OP_ADD_UNICODE, 182, 40, {0}},
    [_BINARY_OP_INPLACE_ADD_UNICODE] = {emit__BINARY_OP_INPLACE_ADD_UNICODE, 173, 64, {0}},
    [_BINARY_OP_MULTIPLY_FLOAT] = {emit__BINARY_OP_MULTIPLY_FLOAT, 52, 24, {0}},
    [_BINARY_OP_MULTIPLY_INT] = {emit__BINARY_OP_MULTIPLY_INT, 182, 40, {0}},
    [_BINARY_OP_SUBTRACT_FLOAT] = {emit__BINARY_OP_SUBTRACT_FLOAT, 52, 24, {0}},
    ...
}

官方提供了两种安装方式,一种是通过 apt 源来安装;另一种是通过官方提供的 shell 脚本来执行安装。

通过官方提供的源来安装 clang-19

编辑 sudo vim /etc/apt/sources.list.d/llvm.list 文件,将如下文本粘贴至文件中,保存后执行 sudo apt update,然后就可以去执行 sudo apt install clang-19。方法很简单,但是事与愿违,执行脚本后,输出内容会告诉你缺少各种依赖,看着就头大。

Noble (24.04) - Last update : Sun, 24 Nov 2024 12:29:55 UTC / Revision: 20241124082213+eb4d2f24a724
deb http://apt.llvm.org/noble/ llvm-toolchain-noble main
deb-src http://apt.llvm.org/noble/ llvm-toolchain-noble main
# 18
deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main
deb-src http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main
# 19
deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main
deb-src http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main

sudo ./llvm.sh 19 all

如下 shell 是官方提供的一个简易的安装方式,把 shell 下载下来,添加可执行权限,然后用 root 权限执行安装。同样的你会发现执行不下去,安装失败。

bash
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh <version number> all
# or
sudo ./llvm.sh all

周五反复折腾了几次这两种方法,然后正好遇到周末休息一下,也就没有继续折腾了。这会儿突然来了一个点子,既然编译环境安装这么麻烦,我们不是有 docker 这种编译好的现成的环境吗?于是抱着试一试的心态继续折腾一下,皇天不负有心人,我在 docker 的 hub 中搜到了 docker pull silkeh/clang:19。有个这个不就好办了吗,之前 Python 的源码已经下载好了,我只需要制作一个 docker-compose.yml 文件即可。

bash
version: '3.7'
services: 
  python314:
    image: silkeh/clang:19
    volumes:
      - .:/src
    working_dir: /src
    command: 
      - /bin/bash
      - -c
      - |
        ./configure --enable-experimental-jit
        make

将文件 docker-compose.yml 放置在 Python 源码的根目录下,我只需要执行一下 sudo docker-compose up 就可以进入编译。很快就在 Python 的根目录下生成了 Python 的可执行文件,比较幸运的是 docker 编译的二进制文件在物理机上也可以执行。

bash
./python
Python 3.14.0a2 (main, Nov 24 2024, 16:11:20) [Clang 19.1.4 (++20241112103742+d174e2a55389-1~exp1~20241112103841.63)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>