9.5 例子:在列表中添加元素
解析列表添加元素涉及的底层操作
在 Python 中,当你创建了一个列表,你就可以在这个列表对象上使用 append()
方法:
在这个例子中, obj
是你要添加到列表末尾的对象。
这段代码涉及到了两种字节码操作:
使用
LOAD_FAST
将obj
从帧中的locals
列表加载到值栈的顶部;使用
LIST_APPEND
去添加这个对象。
LOAD_FAST
包括以下 5 步:
首先要通过
GETLOCAL()
加载指向obj
的指针,其中要加载的变量是操作码的参数。需要加载的变量指针存储在fastlocals
列表中,它是PyFrame
中f_localsplus
属性的副本。而oparg
是一个数字,指向fastlocals
这个数组指针的索引。这意味着 Python 加载的仅仅是一个局部变量(作为指针的副本),而不需要查找变量名;如果这个变量已经不存在了,将会引发局部变量未绑定的错误;
加载的变量
value
(在这个例子中指obj
)的引用计数加 1;将指向
obj
的指针压入值栈的顶部;调用宏
FAST_DISPATCH
,如果启用了栈帧跟踪的功能,循环将再次执行去跟踪栈帧的信息。如果没有使能栈帧跟踪的功能,就会直接使用goto
跳转到fast_next_opcode
。此时goto
会重新跳转到求值循环的顶部去执行下一条指令。
以下是 LOAD_FAST
中 5 个步骤对应的代码:
现在指向 obj
的指针置于值栈的顶部,下一条指令 LIST_APPEND
就可以开始执行了。
许多字节码操作都会引用基础数据类型,例如 PyUnicode
、PyNubmer
等。在这个例子中,LIST_APPEND
要添加一个对象到列表的末尾。为了完成这项工作,需要先从值栈中弹出最后一个对象的指针并将这个指针返回。
这个宏可以用以下方式实现:
现在执行 obj
的指针储存到了变量 v
中。列表的指针由 PEEK(oparg)
加载。
然后以 list
和 v
作为参数,调用 Python 列表的 C API 添加元素。具体的代码在 Objects/listobject.c
文件中,你可以在“对象与类型”这一章学习到更多细节。
随后会调用 PREDICT
宏进行指令预测,它猜测下一条指令将会是 JUMP_ABSOLUTE
。PREDICT
宏中包含了编译器生成的 goto
语句,可以用于跳转到有可能被执行的 case
语句。
这也意味着 CPU 可以跳转到这条指令,而不需要再走一次循环判断的流程:
注
有些字节码是成对出现的,这使得在第一个操作执行时预测下一个操作成为可能。例如,COMPARE_OP
后面常常紧接着 POP_JUMP_IF_FALSE
或 POP_JUMP_IF_TRUE
。
验证预测的效果需要对寄存器变量内的常量进行高速测试。如果操作码配对测试的效果很好,则说明处理器在分支内部预测的成功率很高,这会让下一个 opcode
的执行开销接近为 0。成功的指令预测可以避免再次执行求值循环的过程和那些不可预测的 switch-case
语句分支。结合处理器内部的分支预测功能,一次成功的 PREDICT
可以让两个 opcode
连续执行,就像它们组成了一个新的 opcode
并把函数体结合到一起。
如果想统计操作码的执行信息,你有两个选择:
保持预测功能开启,如果有些操作码结合在一起执行,就把它们整体作为统计的结果;
关闭预测功能,以便统计每一个预测码的执行次数。
可以使用线程代码禁用操作码预测功能,因为线程代码允许 CPU 去记录每一个操作码的分支预测信息。
有一些操作码会引用编译后的函数作为参数,例如 CALL_FUNCTION
和 CALL_METHOD
。这种情况下,会将一个新的帧被压入当前线程的帧栈中,同时在求值循环执行该函数,直到该函数结束。每当创建一个新的帧并把它压入栈时,帧中 f_back
字段的值都会在创建新帧之前被设置为当前帧。
当你看到堆栈跟踪结果时,就可以很清晰的看到帧的嵌套关系了。
在命令行中执行上述代码块,你将会得到:
可以使用 Lib\traceback.py
中的 walk_stack()
函数回溯栈帧的信息:
将当前帧父节点的父节点 (sys._getframe().f_back.f_back
) 作为回溯的基准,因为我们不想在栈帧的回溯中看到 walk_stack()
和 print_trace()
的信息。f_back
指针跟随调用栈的顶部。
sys._getframe()
是一个 Python API,它可以获取当前线程的 frame
属性。
以下是帧栈可视化后的结果,以下 3 个帧每个都有它自己独立的 code object,同时线程状态指针指向当前正在执行的帧。
Last updated