看书 Python 源码分析笔记 (五)

第9章 Python 虚拟机中的一般表达式

 

这里一般表达式指对象创建语句, 打印语句等. 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.

 

================================================================

第10章 Python 虚拟机中的控制流

 

Python 虚拟机中的 if 控制流

示例 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 怎么整...?

Python 虚拟机中的 for 循环控制流

前面见到了 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 用于指令的回退. 这里使用绝对地址, 有点奇怪...

Python 虚拟机中 while 循环控制结构

可能看看 break 和 continue 的实现就够了吧.
continue 在 python 中用 JUMP_ABSOLUTE 实现.
break 实现通过设置 why 变量值为 WHY_BREAK 来实现, 主要是恢复 block 中信息.

由于 break, continue 等需要的信息都可以编译期计算出来, 这里用动态的 block 方法实现, 是否有效率呢?

 

Python 虚拟机中的异常控制流

考察 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 意思)
异常处理是语言中较复杂的一块, 值得仔细学习其实现. (后面细节暂时略)

你可能感兴趣的:(看书 Python 源码分析笔记 (五))