0. 参考资料
参考资料如下:
- B站: 【python】字节码和虚拟机?python代码竟然是这么执行的!
- python的编译字节码流程: PEP 3147 – PYC Repository Directories
1. 使用字节码(ByteCode)
1.1. 总述
在阅读本文之前,需要先知道python运行代码时候的基本逻辑:
在执行python文件时候,第一步: python解释器会将你写的python代码先编译为字节码
第二步: 当你每一次调用函数,或者刚开始运行python的时候,cpython会建立一个新的Frame,然后在这个Frame框架下,cpython会一条一条的执行编译后的ByteCode, 每一条ByteCode在C语言中有相应的代码去执行它。
另外,在每一个Frame里, cpython都会维护一个stack,然后ByteCode会和这个Stack进行交互操作。
1.2. 使用字节码入门
我们先在交互式界面定义一个最简单的add函数, 查看它的字节码,如下:
xd@wxd:~$ bpython
bpython version 0.18 on top of Python 3.8.10 /usr/bin/python3
>>> def func(a, b):
... return a + b
...
>>> from dis import dis
>>> dis(func)
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
>>>
本文不介绍python标准库
dis
的使用, 如果不了解,请先参考我写的dis
模块的使用文档。
在输出的结果中一共四行:
- 前两行操作码都是
LOAD_FAST
, 加载了两个不同的变量 - 第三行是执行加法运算的操作码:
BINARY_ADD
- 最后一行是返回值的操作码:
RETURN_VALUE
,每个函数都会有这个操作码,即使你没有写return语句
1.3. 这三个操作码的含义
上面的三个操作码(opcode), 官方的解释:
LOAD_FAST
: https://docs.python.org/3/library/dis.html#opcode-LOAD_FASTBINARY_ADD
: https://docs.python.org/3.8/library/dis.html#opcode-BINARY_ADDRETURN_VALUE
: https://docs.python.org/3/library/dis.html#opcode-RETURN_VALUE
1.3.1. 其中LOAD_FAST(var_num)
作用
Pushes a reference to the local
co_varnames[var_num]
onto the stack.
翻译如下:
将对本地变量co_varnames[var_num]
的引用压到栈里面。
1.3.2. 其中BINARY_ADD
作用
Implements
TOS
=TOS1
+TOS
.
翻译如下:
实现了TOS
=TOS1
+TOS
.
TOS
就是Top of Stack
, 也就是栈顶。
1.3.3. 其中RETURN_VALUE
作用
Returns with
TOS
to the caller of the function.
翻译如下:
将栈顶的数据返回给函数的调用者
2. cpython的实现
上面从官方文档中查看了不同操作码的含义,这部分我们深入到Cpython源码中,查看具体的实现过程。
说明:下面所有的代码都摘录自: cpython源码中3.8分支的代码; 不同分支中的c代码实现可能不同
2.1. Cpython中字节码的代码文件
cpython中处理与字节码有关的文件是:Python/ceval.c
, 对应的头文件路径:Include/ceval.h
。
在Python/ceval.c
文件中的_PyEval_EvalFrameDefault
函数是字节码执行的最核心的逻辑,所以这个函数非常重。
但是_PyEval_EvalFrameDefault
函数也非常长,不是很容易阅读,所以下面将重点部分摘录下来,以便理解。
2.2 常用的宏定义
在Python/ceval.c
文件中的_PyEval_EvalFrameDefault
函数中,定义了很多宏,我们不可能每一个都解释一下,这里摘录了几个非常常用的宏:
2.2.1. 宏FAST_DISPATCH
#define FAST_DISPATCH() goto fast_next_opcode
这个宏的作用就是跳转到下一个操作码。
2.2.2. 与栈操作有关的一系列宏
cpython中定义了很多操作栈的宏,这些宏非常常用,摘录了部分,如下:
/* Stack manipulation macros */
/* The stack can grow at most MAXINT deep, as co_nlocals and
co_stacksize are ints. */
#define STACK_LEVEL() ((int)(stack_pointer - f->f_valuestack))
#define EMPTY() (STACK_LEVEL() == 0)
#define TOP() (stack_pointer[-1])
#define SECOND() (stack_pointer[-2])
#define THIRD() (stack_pointer[-3])
#define FOURTH() (stack_pointer[-4])
#define PEEK(n) (stack_pointer[-(n)])
#define SET_TOP(v) (stack_pointer[-1] = (v))
#define SET_SECOND(v) (stack_pointer[-2] = (v))
#define SET_THIRD(v) (stack_pointer[-3] = (v))
#define SET_FOURTH(v) (stack_pointer[-4] = (v))
#define SET_VALUE(n, v) (stack_pointer[-(n)] = (v))
#define BASIC_STACKADJ(n) (stack_pointer += n)
#define BASIC_PUSH(v) (*stack_pointer++ = (v))
#define BASIC_POP() (*--stack_pointer)
#ifdef LLTRACE
#define PUSH(v) { (void)(BASIC_PUSH(v), \
lltrace && prtrace(tstate, TOP(), "push")); \
assert(STACK_LEVEL() <= co->co_stacksize); }
#define POP() ((void)(lltrace && prtrace(tstate, TOP(), "pop")), \
BASIC_POP())
#define STACK_GROW(n) do { \
assert(n >= 0); \
(void)(BASIC_STACKADJ(n), \
lltrace && prtrace(tstate, TOP(), "stackadj")); \
assert(STACK_LEVEL() <= co->co_stacksize); \
} while (0)
#define STACK_SHRINK(n) do { \
assert(n >= 0); \
(void)(lltrace && prtrace(tstate, TOP(), "stackadj")); \
(void)(BASIC_STACKADJ(-n)); \
assert(STACK_LEVEL() <= co->co_stacksize); \
} while (0)
#define EXT_POP(STACK_POINTER) ((void)(lltrace && \
prtrace(tstate, (STACK_POINTER)[-1], "ext_pop")), \
*--(STACK_POINTER))
#else
#define PUSH(v) BASIC_PUSH(v)
#define POP() BASIC_POP()
#define STACK_GROW(n) BASIC_STACKADJ(n)
#define STACK_SHRINK(n) BASIC_STACKADJ(-n)
#define EXT_POP(STACK_POINTER) (*--(STACK_POINTER))
#endif
2.2.3. 宏GETLOCAL
在_PyEval_EvalFrameDefault
函数中,还有一个常用的宏如下:
/* Local variable macros */
#define GETLOCAL(i) (fastlocals[i])
2.3 主循环与主switch
语句
在Python/ceval.c
文件中的_PyEval_EvalFrameDefault
函数中,最关键的代码就是一个主循环,这个主循环的作用是不停的解析每一个收到的字节码,直到遇到退出条件。 如下:
main_loop:
for (;;) {
assert(stack_pointer >= f->f_valuestack); /* else underflow */
assert(STACK_LEVEL() <= co->co_stacksize); /* else overflow */
assert(!_PyErr_Occurred(tstate));
// 后面省略了很多内容
这个主循环的关键部分是一个巨大的switch
语句,针对不同opcode
跳转到不同的代码段进行执行,这是非常核心的一段代码。 如下:
switch (opcode) {
/* BEWARE!
It is essential that any operation that fails must goto error
and that all operation that succeed call [FAST_]DISPATCH() ! */
case TARGET(NOP): {
FAST_DISPATCH();
}
// 后面省略了很多case的内容
2.4. 介绍LOAD_FAST
:
我们前面介绍的LOAD_FAST(var_num)
就在这个switch的前面, 如下:
case TARGET(LOAD_FAST): {
PyObject *value = GETLOCAL(oparg);
if (value == NULL) {
format_exc_check_arg(tstate, PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
goto error;
}
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
下面分别介绍:
PyObject *value = GETLOCAL(oparg);
: 将oparg
(也就是操作符的参数)取出来, 赋值给value
指针。- 一个判断语句: 如果发现
value
指针为空,做对应的参数检查 Py_INCREF(value);
: 处理引用计数的问题PUSH(value);
: 将value压入栈中, 这是这个case中最核心的代码FAST_DISPATCH();
: 跳转到下一个操作符上,继续进行循环
2.5. 介绍BINARY_ADD
:
同样在_PyEval_EvalFrameDefault
函数中主switch
语句中,opcode BINARY_ADD
的逻辑如下:
case TARGET(BINARY_ADD): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
/* NOTE(haypo): Please don't try to micro-optimize int+int on
CPython using bytecode, it is simply worthless.
See http://bugs.python.org/issue21955 and
http://bugs.python.org/issue10044 for the discussion. In short,
no patch shown any impact on a realistic benchmark, only a minor
speedup on microbenchmarks. */
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(tstate, left, right, f, next_instr);
/* unicode_concatenate consumed the ref to left */
}
else {
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
下面分别介绍:
PyObject *right = POP();
: 弹出栈顶的元素,并将其复制为right
指针PyObject *left = TOP();
: 获取现在栈顶的元素,并将其复制为left
指针PyObject *sum;
: 定义一个求和指针sum
sum = PyNumber_Add(left, right);
: 核心的求和代码SET_TOP(sum);
: 将栈顶元素设置为求出的和sum
在文件Objects/abstract.c
中定义了关键方法PyNumber_Add
的实现:
PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
{
PyObject *result = binary_op1(v, w, NB_SLOT(nb_add));
if (result == Py_NotImplemented) {
PySequenceMethods *m = v->ob_type->tp_as_sequence;
Py_DECREF(result);
if (m && m->sq_concat) {
return (*m->sq_concat)(v, w);
}
result = binop_type_error(v, w, "+");
}
return result;
}
2.6. 介绍RETURN_VALUE
:
同样在_PyEval_EvalFrameDefault
函数中主switch
语句中,opcode RETURN_VALUE
的逻辑如下:
case TARGET(RETURN_VALUE): {
retval = POP();
assert(f->f_iblock == 0);
goto exit_returning;
}
这个比较简单,不介绍了。