Post

Python 代码编译运行过程(2):Python 虚拟机

简单总结一下 Python 代码编译运行过程

前言

阅读 Pytorch 源码时涉及到 Python 代码编译执行相关的内容,为了便于理解,简单学习了 Inside The Python Virtual Machine 的部分内容,本文内容主要来自于此,相关细节请参考原文。

Code Objects

Python 程序由代码块构成,交互模式下的每个命令、脚本文件都是代码块,当 Python 编译代码块时,都会生成 code object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0: 
        return 'FizzBuzz'
    elif n % 3 == 0: 
        return 'Fizz'
    elif n % 5 == 0: 
        return 'Buzz'
    else: 
        return str(n)

# 打印代码对象内容
for attr in dir(fizzbuzz.__code__):
    if attr.startswith('co_'):
        print(f"{attr}:\t{getattr(fizzbuzz.__code__, attr)}")

# 反编译得到的字节码指令
import dis
dis.dis(fizzbuzz)

上面代码的打印结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# ===================代码对象内容
co_argcount:    1
co_cellvars:    ()
co_code:        b'|\x00d\x01\x16\x00d\x02k\x02r\x1c|\x00d\x03\x16\x00d\x02k\x02r\x1cd\x04S\x00|\x00d\x01\x16\x00d\x02k\x02r,d\x05S\x00|\x00d\x03\x16\x00d\x02k\x02r<d\x06S\x00t\x00|\x00\x83\x01S\x00d\x00S\x00'
co_consts:      (None, 3, 0, 5, 'FizzBuzz', 'Fizz', 'Buzz')
co_filename:    <ipython-input-1-2a4d59ff6c50>
co_firstlineno: 1
co_flags:       67
co_freevars:    ()
co_kwonlyargcount:      0
co_lnotab:      b'\x00\x01\x18\x01\x04\x01\x0c\x01\x04\x01\x0c\x01\x04\x02'
co_name:        fizzbuzz
co_names:       ('str',)
co_nlocals:     1
co_posonlyargcount:     0
co_stacksize:   2
co_varnames:    ('n',)

# ===================字节码指令
  2           0 LOAD_FAST                0 (n)
              2 LOAD_CONST               1 (3)
              4 BINARY_MODULO
              6 LOAD_CONST               2 (0)
              8 COMPARE_OP               2 (==)
             10 POP_JUMP_IF_FALSE       28
             12 LOAD_FAST                0 (n)
             14 LOAD_CONST               3 (5)
             16 BINARY_MODULO
             18 LOAD_CONST               2 (0)
             20 COMPARE_OP               2 (==)
             22 POP_JUMP_IF_FALSE       28

  3          24 LOAD_CONST               4 ('FizzBuzz')
             26 RETURN_VALUE

  4     >>   28 LOAD_FAST                0 (n)
             30 LOAD_CONST               1 (3)
             32 BINARY_MODULO
             34 LOAD_CONST               2 (0)
             36 COMPARE_OP               2 (==)
             38 POP_JUMP_IF_FALSE       44

  5          40 LOAD_CONST               5 ('Fizz')
             42 RETURN_VALUE

  6     >>   44 LOAD_FAST                0 (n)
             46 LOAD_CONST               3 (5)
             48 BINARY_MODULO
             50 LOAD_CONST               2 (0)
             52 COMPARE_OP               2 (==)
             54 POP_JUMP_IF_FALSE       60

  7          56 LOAD_CONST               6 ('Buzz')
             58 RETURN_VALUE

  9     >>   60 LOAD_GLOBAL              0 (str)
             62 LOAD_FAST                0 (n)
             64 CALL_FUNCTION            1
             66 RETURN_VALUE
             68 LOAD_CONST               0 (None)
             70 RETURN_VALUE
  • co_code:这些字节码指令序列中的每一个都由一个操作码(opcode)和一个参数(opatg)组成。 例如,co.co_code[0] 返回指令的第一个字节 124,该字节对应于 python 的 LOAD_FAST 操作码。使用dis.dis(co_code)即可以得到字节码指令。
  • co_consts:常量列表,字节码里面的 LOAD_CONST 1(3)即是加载常量列表索引为1的值,即3。
  • co_firstlineno:代码对象第一行行号。
  • co_flag:该字段指示代码对象的种类。 例如,当代码对象是协程的对象时,该标志设置为 0x0080。 还有其他标志,例如 CO_NESTED 指示一个代码对象是否嵌套在另一个代码块内,CO_VARARGS 指示一个代码块是否具有变量自变量,等等。 这些标志会影响字节码执行期间求值循环的行为。

Frames Objects

code object 包含可执行的字节代码,但缺少执行此类代码所需的上下文信息。可以将 frame object 视为执行代码对象的容器,并且它引用了某些代码对象执行期间所需的数据和值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* previous frame, or NULL
                                 该字段是对在当前代码对象之前执行的代码对象所在 frame 的引用。
                                 给定一组 frame 对象,这些 frame 的 f_back 字段一起组成一个 frame 栈结构,这些 frame 一直返回到初始 frame。
                                 然后,初始 frame 在 f_back 字段的值为 NULL 。 这种隐式 frame 堆栈形成了我们称为调用堆栈的 frame。*/
    PyCodeObject *f_code;       /* code segment
                                 该字段是对代码对象的引用。
                                 此代码对象包含在该 frame 的上下文中执行的字节码。*/
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) 
                                 这是对内置名称空间的引用。 该名称空间包含诸如 print,enumerate 等名称及其对应的值。*/
    PyObject *f_globals;        /* global symbol table (PyDictObject) 这是对代码对象的全局名称空间的引用。*/
    PyObject *f_locals;         /* local symbol table (any mapping) 
                                 这是对代码对象的局部名称空间的引用。 
                                 如前所述,这些名称已在函数范围内定义。 
                                 当我们讨论 f_localplus 字段时,我们将看到 python 在使用局部定义的名称时所做的优化。*/
    PyObject **f_valuestack;    /* points after the last local 
                                 这是对 frame 求值堆栈的引用。 
                                 回想一下,Python 虚拟机是基于堆栈的虚拟机,因此在对字节码进行求值期间,将从堆栈的顶部读取值,并将字节码求值的结果存储在堆栈的顶部。 
                                 该字段是在代码对象执行期间使用的堆栈。 
                                 frame 代码对象的堆栈大小提供了此数据结构可以扩展到的最大深度。*/
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    PyObject **f_stacktop;
    PyObject *f_trace;          /* Trace function */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */

    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;

    int f_lasti;                /* Last instruction if called */
    /* Call PyFrame_GetLineNumber() instead of reading this field
       directly.  As of 2.3 f_lineno is only valid when tracing is
       active (i.e. when f_trace is set).  At other times we use
       PyCode_Addr2Line to calculate the line from the current
       bytecode index. */
    int f_lineno;               /* Current line number */
    int f_iblock;               /* index in f_blockstack */
    char f_executing;           /* whether the frame is still executing */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

指令执行

frame 对象执行情况可以参考下图

1678366804568

frame 中的指令最终由 _PyEval_EvalFrameDefault 函数执行,函数内部主要是 switch-case 逻辑,执行对应的指令

1
2
3
4
5
6
... 
   switch (opcode) {
        case TARGET(LOAD_CONST): {      /* 加载常量 */
            ...
        }
 ...
This post is licensed under CC BY 4.0 by the author.