这里一般表达式指对象创建语句, 打印语句等. if, while 等归为控制流语句于下一章.
示例 python 代码:
i = 1 // int object s = "Python" // string object d = {} // dict object l = [] // list object
在 Python 虚拟机执行函数中定义了大量的宏, 为此章分析, 需要有所了解(略, 看书).
如上示例语句 i = 1 对应的字节码为:
i = 1 // Python 代码 0 LOAD_CONST 0 (1) // 字节码 3 STORE_NAME 0 (i)
根据经验, LOAD_CONST 从常量表中加载一个常量到栈顶, 指令参数应为常量表的索引. 故而指令总长 3 个字节.
对应 C 实现基本上简明易懂:
[LOAD_CONST] // in PyEval_EvalFrameEx() x = GETITEM(consts, oparg); // 从常量表中取索引为 oparg 的项. Py_INCREF(x); // 增加该对象的引用计数. PUSH(x); // 压入堆栈.
指令 STORE_NAME 实现类似. 即在 frame->locals 命名空间建立 i=1 的关联(或叫约束, 或叫绑定).
其它 python 语句与对应的字节码也比较易懂:
s = "Python" 6 LOAD_CONST 1 ('Python') 9 STORE_NAME 1 (s) // 建立 s=stack_TOP='Python' 的关联 d = {} 12 BUILD_MAP 0 // 新建 map/dict 对象 15 STORE_NAME 2 (d) l = [] 18 BUILD_LIST 0 21 STORE_NAME 3 (l)
将上面例子中的 d, l 改动一下:
d = {"1":1, "2": 2}
l = [1, 2]
会产生新的一些用于初始化 dict/list 的字节码. 相关指令有 DUP_TOP(估计是复制栈顶元素), ROT_TWO(对调
栈顶两个元素), STORE_SUBSCR(估计实现 d[idx]=val 初始化). 这里 4 个指令一组实现 dict[key]=val.
list 对象稍稍有点不同, 其先堆栈中压入数个元素, 然后 BUILD_LIST 2 一次性构造出 list 元素.
(隐含的问题: 压栈太多? 是否会分成几次添加列表元素?)
待分析代码及编译的字节码示例:
a = 5 b = a 0 LOAD_NAME 0 (a) // 执行 LGB 规则搜索变量 a 3 STORE_NAME 1 (b) c = a + b LOAD_NAME 'a' LOAD_NAME 'b' BINARY_ADD // 运算指令 '+' STORE_NAME 'c' print c LOAD_NAME 'c' PRINT_ITEM PRINT_NEWLINE // 没想到打印 \n 也有单独指令...
这里 LOAD_NAME 首先搜索 local 命名空间, 若未找到则搜索 global 命名空间, 再次 builtins.
也即执行 LGB 规则. (搜索是耗时巨大的操作, 如果说 python 慢, 那这里一定就是原因之一了)
指令 BINARY_ADD (或其它类似数学/逻辑运算指令) 有对 int, string 处理的快速检查和处理, 否则调用更复杂的
PyNumber_Add() 等方法执行运算.
指令 PRINT_ITEM 将对象输出到 stream, 缺省为 stdout 类似的 stream.
================================================================
示例 python 代码:
a = 1 // or 2, 3, 4 if a > 10: // or a < 0, a == 1, a != 2 etc. print "there there!" elif ... else ...
大致翻译过来的字节码:
if a > 10: LOAD_NAME 'a' LOAD_CONST 10 COMPARE_OP '>' JUMP_IF_FALSE to_27 POP_TOP print 'there there...' 此指令字节码略 to_27: 27 POP_TOP elif ... 类似上面, COMPARE_OP 参数为 '<', '<=', '>=', '==', '!=' 等. ... JUMP_FORWARD to_99 ...
指令 COMPARE_OP 也是一种二元运算符, 和 ADD 等类似, 因此不多研究了.
关键点是 JUMP_IF_FALSE (估计也有 JUMP_IF_TRUE) 指令, 以及 JUMP_FORWARD 指令.
Python 的字节码跳转指令实现颇有特色. 在 python 中有一些指令经常成对(顺序)出现, 这为根据上一个指令
预测下一条指令提供了可能. 如 COMPARE 后面常跟着有 JUMP_IF_TRUE/FALSE, 又后面常跟 POP_TOP 指令.
因此 python 虚拟机提供了字节码预测功能, 如果预测成功, 那么会省去很多无用操作, 提升执行效率.
宏 PREDICT(op) 即判定下一条指令是否为 op, 如果是则跳转到该 op 分支的地方执行.
在 python 源码的注释中提及可以做一些 profile 工作, 看看这种 predict 优化到底能提升多少效率...
为提升几个纳秒的执行速度, 程序猿们花费了巨大努力! 当使用这些语言写出低劣的代码时, 会多对不起他们?
// ceval.c 预测跳转示例. case JUMP_IF_FALSE: // switch (op) ... w = TOP(); if (w == Py_True) PREDICT(POP_TOP); // 预测下一条指令如果是 POP_TOP 则跳转. ...
指令 JUMP_IF_FALSE 实现为指令寄存器 (ip) 增减一个相对距离, 即它们是相对跳转. 同样 JUMP_FORWARD
也是相对跳转.
看源码还有 JUMP_ABSOLUTE 指令是根据参数 oparg 执行绝对跳转, 我有点好奇什么地方会用到绝对跳转?
因为一个``好的''代码应该和它的相对位置(加载地址)无关, 不然 ASLR 怎么整...?
前面见到了 JUMP_FORWARD 是向前跳跃, 那 for 会导致的指令向回跳跃会有 JUMP_BACKWARD 吗?
示例 python 代码及字节码:
// for-test.py lst = [1,2] for i in lst: 12 SETUP_LOOP 19 (to 34) 15 LOAD_NAME 'list' 18 GET_ITER 19 FOR_ITER 11 (to 33) 22 STORE_NAME 'i' print i 25 LOAD_NAME 'i' 28 PRINT_ITEM 29 PRINT_NEWLINE 30 JUMP_ABSOLUTE 19 33 POP_BLOCK 34 end ...
这里指令 SETUP_LOOP 用于初始化 for 循环控制. 这里会使用 frame 中的 blockstack[] 结构:
struct PyTryBlock { int b_type; // 此块的类型 int b_handler; // where to jump to find handler int b_level; // value stack level to pop to }
在 frame 中:
struct PyFrameObject { ... // for try and loop blocks PyTryBlock f_blockstack[CO_MAXBLOCKS=20]; ... }
循环设置指令 SETUP_LOOP 从数组中取得一个 PyTryBlock, 初始化它的值. 这个块也用于 try/except/finally
异常处理中使用. (这里使用 block 以支持 loop 指令我觉得可以和别的语言实现进行比较)
指令 GET_ITER, FOR_ITER 看名字知道与迭代/循环有关. 在 python 中迭代器实现为一个对象, 例如 list 的
迭代器为 listiterobject.
指令 FOR_ITER 获取 iter 的下一个元素并压入堆栈.
指令 JUMP_ABSOLUTE 用于指令的回退. 这里使用绝对地址, 有点奇怪...
可能看看 break 和 continue 的实现就够了吧.
continue 在 python 中用 JUMP_ABSOLUTE 实现.
break 实现通过设置 why 变量值为 WHY_BREAK 来实现, 主要是恢复 block 中信息.
由于 break, continue 等需要的信息都可以编译期计算出来, 这里用动态的 block 方法实现, 是否有效率呢?
考察 python 异常的简单例子:
1/0 # LOAD_CONST 0 # LOAD_CONST 1 # BINARY_DIVIDE // 这里除 0 抛出异常
由于除 0 在 python 中抛出异常 ZeroDivideError. 实现是在指令 BINARY_DIVIDE 调用的方法
PyNumber_Divide() 中抛出. 具体细节如下:
1. PyNumber_Divide() 检查发现除数是 0, 设置异常对象于 thread 中, 并返回 NULL.
2. 指令 BINARY_DIVIDE 发现返回值为 NULL, 则退出 switch, 进入异常处理部分.
// 除法最终在这里实现. type i_divmod(x, y, ...) { if (y == 0) { // 抛出异常. PyErr_SetString(PyExc_ZeroDivisionError, "division by zero"); return ERROR! } }
函数 PyErr_SetString() 将异常信息保存在当前线程中(这里是关键点):
// PyErr_SetString() 最终调用到这里. void PyErr_Restore(PyObject *type, PyObject *value, ...) { PyThreadState thread = get-current-thread(); thread->current_exception = exception(type, value, ...); }
在跳出虚拟机大 switch 分发语句之后, 检查 x 的值为 NULL, 则表示有某种异常情况发生. 此时设置状态变量
why = WHY_EXCEPTION, 然后进入异常处理流程.
异常处理创建 traceback 对象, 其与 frame 对象链恰好是反着的.
Python 虚拟机当遇到异常抛出, 会在当前栈帧中寻找 except 语句, 如果没有, 则退出当前活动栈帧(回退到前一个
栈帧). 并一直回退直到 python 自己处理, 如果用户代码没有处理的话. 随着回退, traceback 链不断构造出来.
PyEval_EvalFrameEx() 是递归调用的, 递归与链表结构相对应. (如果没有必要, 也可以不用递归)
沿着栈帧链不断回退的过程称之为栈帧展开. (应就是 rewind 意思)
异常处理是语言中较复杂的一块, 值得仔细学习其实现. (后面细节暂时略)