python的字节码(ByteCode)

0. 参考资料

参考资料如下:

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模块的使用文档。

在输出的结果中一共四行:

  1. 前两行操作码都是LOAD_FAST, 加载了两个不同的变量
  2. 第三行是执行加法运算的操作码: BINARY_ADD
  3. 最后一行是返回值的操作码: RETURN_VALUE,每个函数都会有这个操作码,即使你没有写return语句

1.3. 这三个操作码的含义

上面的三个操作码(opcode), 官方的解释:

  • LOAD_FAST: https://docs.python.org/3/library/dis.html#opcode-LOAD_FAST
  • BINARY_ADD: https://docs.python.org/3.8/library/dis.html#opcode-BINARY_ADD
  • RETURN_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();
}

下面分别介绍:

  1. PyObject *value = GETLOCAL(oparg);: 将oparg(也就是操作符的参数)取出来, 赋值给value指针。
  2. 一个判断语句: 如果发现value指针为空,做对应的参数检查
  3. Py_INCREF(value);: 处理引用计数的问题
  4. PUSH(value);: 将value压入栈中, 这是这个case中最核心的代码
  5. 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();
}

下面分别介绍:

  1. PyObject *right = POP();: 弹出栈顶的元素,并将其复制为right指针
  2. PyObject *left = TOP();: 获取现在栈顶的元素,并将其复制为left指针
  3. PyObject *sum;: 定义一个求和指针sum
  4. sum = PyNumber_Add(left, right);: 核心的求和代码
  5. 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;
}

这个比较简单,不介绍了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值