Python虚拟机中的if控制流
在所有的编程语言中,if控制流是最简单也是最常用的控制流语句。下面,我们来分析一下在Python虚拟机中对if控制流的实现
# cat demo.py a = 1 if a > 10: print("a>10") elif a <= -2: print("a<=-2") elif a != 1: print("a!=1") elif a == 1: print("a==1") else: print("Unknown a")
我们先扫一下demo.py这个文件,这是一个非常简单的程序,我们的关注点并不在这个程序的本身,而是程序编译后的符号表、常量表、字节码、以及字节码中的实现
首先,我们先来看一下符号表和常量表
# python2.5 …… >>> source = open("demo.py").read() >>> co = compile(source, "demo.py", "exec") >>> co.co_names ('a',) >>> co.co_consts (1, 10, 'a>10', -2, 'a<=-2', 'a!=1', 'a==1', 'Unknown a', None)
其次是字节码
>>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (1) 3 STORE_NAME 0 (a) 2 6 LOAD_NAME 0 (a) 9 LOAD_CONST 1 (10) 12 COMPARE_OP 4 (>) 15 JUMP_IF_FALSE 9 (to 27) 18 POP_TOP 3 19 LOAD_CONST 2 ('a>10') 22 PRINT_ITEM 23 PRINT_NEWLINE 24 JUMP_FORWARD 72 (to 99) >> 27 POP_TOP 4 28 LOAD_NAME 0 (a) 31 LOAD_CONST 3 (-2) 34 COMPARE_OP 1 (<=) 37 JUMP_IF_FALSE 9 (to 49) 40 POP_TOP 5 41 LOAD_CONST 4 ('a<=-2') 44 PRINT_ITEM 45 PRINT_NEWLINE 46 JUMP_FORWARD 50 (to 99) >> 49 POP_TOP 6 50 LOAD_NAME 0 (a) 53 LOAD_CONST 0 (1) 56 COMPARE_OP 3 (!=) 59 JUMP_IF_FALSE 9 (to 71) 62 POP_TOP 7 63 LOAD_CONST 5 ('a!=1') 66 PRINT_ITEM 67 PRINT_NEWLINE 68 JUMP_FORWARD 28 (to 99) >> 71 POP_TOP 8 72 LOAD_NAME 0 (a) 75 LOAD_CONST 0 (1) 78 COMPARE_OP 2 (==) 81 JUMP_IF_FALSE 9 (to 93) 84 POP_TOP 9 85 LOAD_CONST 6 ('a==1') 88 PRINT_ITEM 89 PRINT_NEWLINE 90 JUMP_FORWARD 6 (to 99) >> 93 POP_TOP 11 94 LOAD_CONST 7 ('Unknown a') 97 PRINT_ITEM 98 PRINT_NEWLINE >> 99 LOAD_CONST 8 (None) 102 RETURN_VALUE >>>
咋一看,长长的一串字节码,有点头晕。没关系,现在让我们将字节码和源码对应上
a = 1 """ 0 LOAD_CONST 0 (1) 3 STORE_NAME 0 (a) """ if a > 10: """ 6 LOAD_NAME 0 (a) 9 LOAD_CONST 1 (10) 12 COMPARE_OP 4 (>) 15 JUMP_IF_FALSE 9 (to 27) 18 POP_TOP """ print("a>10") """ 19 LOAD_CONST 2 ('a>10') 22 PRINT_ITEM 23 PRINT_NEWLINE 24 JUMP_FORWARD 72 (to 99) >> 27 POP_TOP """ elif a <= -2: """ 28 LOAD_NAME 0 (a) 31 LOAD_CONST 3 (-2) 34 COMPARE_OP 1 (<=) 37 JUMP_IF_FALSE 9 (to 49) 40 POP_TOP """ print("a<=-2") """ 41 LOAD_CONST 4 ('a<=-2') 44 PRINT_ITEM 45 PRINT_NEWLINE 46 JUMP_FORWARD 50 (to 99) >> 49 POP_TOP """ elif a != 1: """ 50 LOAD_NAME 0 (a) 53 LOAD_CONST 0 (1) 56 COMPARE_OP 3 (!=) 59 JUMP_IF_FALSE 9 (to 71) 62 POP_TOP """ print("a!=1") """ 63 LOAD_CONST 5 ('a!=1') 66 PRINT_ITEM 67 PRINT_NEWLINE 68 JUMP_FORWARD 28 (to 99) >> 71 POP_TOP """ elif a == 1: """ 72 LOAD_NAME 0 (a) 75 LOAD_CONST 0 (1) 78 COMPARE_OP 2 (==) 81 JUMP_IF_FALSE 9 (to 93) 84 POP_TOP """ print("a==1") """ 85 LOAD_CONST 6 ('a==1') 88 PRINT_ITEM 89 PRINT_NEWLINE 90 JUMP_FORWARD 6 (to 99) >> 93 POP_TOP """ else: print("Unknown a") """ 94 LOAD_CONST 7 ('Unknown a') 97 PRINT_ITEM 98 PRINT_NEWLINE >> 99 LOAD_CONST 8 (None) 102 RETURN_VALUE """
仔细观察每个if语句包括elif,可以发现它们都有相同的套路:
- 执行LOAD_NAME指令,从local名字空间中获得变量名a所对应的值
- 执行LOAD_CONST指令,从常量表consts中读取参与该分支判断操作的常量对象
- 执行COMPARE_OP指令,对前面两条指令取得的变量值和变量对象进行比较操作
- 执行JUMP_IF_FALSE指令,根据COMPARE_OP指令的运行结果进行字节码指令的跳跃,如果if语句中包含not关键字,字节码则为JUMP_IF_TRUE
- 执行POP_TOP指令,将之前存入运行时栈的比较结果弹出栈
LOAD_NAME和LOAD_CONST这两个指令没必要再解释,如果有疑问的同学可以去看我的博客Python虚拟机中的一般表达式(一)这一章,下面我们重点来分析一下COMPARE_OP这个指令的实现:
ceval.c
case COMPARE_OP: w = POP(); v = TOP(); //[1]:PyIntObject对象的快速通道 if (PyInt_CheckExact(w) && PyInt_CheckExact(v)) { /* INLINE: cmp(int, int) */ register long a, b; register int res; a = PyInt_AS_LONG(v); b = PyInt_AS_LONG(w); //根据字节码指令的指令参数选择不同的比较操作 switch (oparg) { case PyCmp_LT: res = a < b; break; case PyCmp_LE: res = a <= b; break; case PyCmp_EQ: res = a == b; break; case PyCmp_NE: res = a != b; break; case PyCmp_GT: res = a > b; break; case PyCmp_GE: res = a >= b; break; case PyCmp_IS: res = v == w; break; case PyCmp_IS_NOT: res = v != w; break; default: goto slow_compare; } x = res ? Py_True : Py_False; Py_INCREF(x); } else { //[2]:一般对象的慢速通道 slow_compare: x = cmp_outcome(oparg, v, w); } Py_DECREF(v); Py_DECREF(w); //将比较结果压入运行时栈 SET_TOP(x); if (x == NULL) break; PREDICT(JUMP_IF_FALSE); PREDICT(JUMP_IF_TRUE); continue;
正如在Python虚拟机中的一般表达式(三)中的BINARY_ADD指令一样,Python虚拟机在COMPARE_OP指令的实现中为PyIntObject对象建立了快速通道。如果参与比较操作的两个对象都是PyIntObject对象,直接取得PyIntObject对对应的整数进行比较即可。从上述代码可以看出,Python正是通过COMPARE_OP指令的不同指令参数来选择不同的比较操作
如果两个对象有其一不是PyIntObject对象,那么很不幸,Python虚拟机只能进入比较操作的慢速通道,调用cmp_outcome方法进行常规的比较,性能远不如为PyIntObject建立的快速通道。现在,让我们看一下cmp_outcome的实现
ceval.c
static PyObject * cmp_outcome(int op, register PyObject *v, register PyObject *w) { int res = 0; switch (op) { case PyCmp_IS: res = (v == w); break; case PyCmp_IS_NOT: res = (v != w); break; case PyCmp_IN: res = PySequence_Contains(w, v); if (res < 0) return NULL; break; case PyCmp_NOT_IN: res = PySequence_Contains(w, v); if (res < 0) return NULL; res = !res; break; case PyCmp_EXC_MATCH: res = PyErr_GivenExceptionMatches(v, w); break; default: return PyObject_RichCompare(v, w, op); } v = res ? Py_True : Py_False; Py_INCREF(v); return v; }
cmp_outcome的实现中,可以看到Python的COMPARE_OP指令不仅管辖着两个之间之间的比较操作,而且还覆盖了对象与集合之间关系的判断操作
# cat demo2.py lst = [1, 2, 3] if 1 in lst: print("Found") # python2.5 …… >>> source = open("demo2.py").read() >>> co = compile(source, "demo2.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (1) 3 LOAD_CONST 1 (2) 6 LOAD_CONST 2 (3) 9 BUILD_LIST 3 12 STORE_NAME 0 (lst) 2 15 LOAD_CONST 0 (1) 18 LOAD_NAME 0 (lst) 21 COMPARE_OP 6 (in) 24 JUMP_IF_FALSE 9 (to 36) 27 POP_TOP 3 28 LOAD_CONST 3 ('Found') 31 PRINT_ITEM 32 PRINT_NEWLINE 33 JUMP_FORWARD 1 (to 37) >> 36 POP_TOP >> 37 LOAD_CONST 4 (None) 40 RETURN_VALUE
我们用dis模块解释了demo2.py的字节码,会发现in操作符也会被解释为COMPARE_OP指令,而指令的参数为PyCmp_IN。所以,cmp_outcome实际上主要是处理这些广义上的比较操作,甚至还包括is操作符的实现。对于PyCmp_IN操作,cmp_outcome会委托给PySequence_Contains来判断在序列对象w中是否存在对象v,而对于通常意义上的两个对象之间的大小关系的比较操作,cmp_outcome委托给PyObject_RichCompare进行
object.c
#define RICHCOMPARE(t) (PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE) ? (t)->tp_richcompare : NULL) PyObject * PyObject_RichCompare(PyObject *v, PyObject *w, int op) { PyObject *res; assert(Py_LT <= op && op <= Py_GE); if (Py_EnterRecursiveCall(" in cmp")) return NULL; /* If the types are equal, and not old-style instances, try to get out cheap (don't bother with coercions etc.). */ if (v->ob_type == w->ob_type && !PyInstance_Check(v)) { cmpfunc fcmp; richcmpfunc frich = RICHCOMPARE(v->ob_type); /* If the type has richcmp, try it first. try_rich_compare tries it two-sided, which is not needed since we've a single type only. */ if (frich != NULL) { res = (*frich)(v, w, op); if (res != Py_NotImplemented) goto Done; Py_DECREF(res); } /* No richcmp, or this particular richmp not implemented. Try 3-way cmp. */ fcmp = v->ob_type->tp_compare; if (fcmp != NULL) { int c = (*fcmp)(v, w); c = adjust_tp_compare(c); if (c == -2) { res = NULL; goto Done; } res = convert_3way_to_object(op, c); goto Done; } } /* Fast path not taken, or couldn't deliver a useful result. */ res = do_richcmp(v, w, op); Done: Py_LeaveRecursiveCall(); return res; }
在PyObject_RichCompare中,首先会确保执行的比较操作介于Py_LT和Py_GE之间,即常规意义上的比较操作。如果进行比较操作的两个对象类型相同,且这两个对象不是用户自定义的类的实例对象,那么首先会选择对象对应的PyTypeObject对象中所定义的tp_richcompare操作。在Python中,无论是Python内建对象,还是用户自定义的类的实例对象,其比较操作都是在各自对应的类型对象中的tp_richcompare或tp_compare中定义的。如果这两个操作都没有成功,Python还会调用do_richcmp进行比较
比较操作的结果——Python中的bool对象
在很多编程语言中,比较操作的结果通常会是一个bool值,即使在没有内建bool值的C中,也会使用0和1来代替bool值Python虚拟机中也有这样两个对立而统一类型的代表成功或失败的PyObject对象:Py_True和Py_False。注意,这两个对象确实是PyObject对象
boolobject.h
/* Don't use these directly */ PyAPI_DATA(PyIntObject) _Py_ZeroStruct, _Py_TrueStruct; /* Use these macros */ #define Py_False ((PyObject *) &_Py_ZeroStruct) #define Py_True ((PyObject *) &_Py_TrueStruct)
和C语言所采用的策略类似,Python也是利用两个PyIntObject对象来充当bool对象
boolobject.c
/* The type object for bool. Note that this cannot be subclassed! */ PyTypeObject PyBool_Type = { PyObject_HEAD_INIT(&PyType_Type) 0, "bool", sizeof(PyIntObject), 0, 0, /* tp_dealloc */ (printfunc)bool_print, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ (reprfunc)bool_repr, /* tp_repr */ &bool_as_number, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ (reprfunc)bool_repr, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES, /* tp_flags */ bool_doc, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ 0, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ &PyInt_Type, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ bool_new, /* tp_new */ }; /* The objects representing bool values False and True */ /* Named Zero for link-level compatibility */ PyIntObject _Py_ZeroStruct = { PyObject_HEAD_INIT(&PyBool_Type) 0 }; PyIntObject _Py_TrueStruct = { PyObject_HEAD_INIT(&PyBool_Type) 1 };
现在,让我们回到demo.py文件和COMPARE_OP,看看demo.py从if a > 10开始执行,到COMPARE_OP指令完成时这段时间内运行时栈的变化:
图1-1 比较操作过程中运行时栈的变化
指令跳跃
在demo.py中,如果第一个判断a > 10成立,那么会立即执行接下来的print语句,如果判断不成立,则跳过print语句,执行下一个判断a <= -2。所以,这里有一个指令跳跃的动作。Python虚拟机中的字节码指令跳跃是如何实现的呢?答案就在COMPARE_OP指令的实现中最后的那个PREDICT宏
ceval.c
#define PREDICT(op) if (*next_instr == op) goto PRED_##op #define PREDICTED(op) PRED_##op: next_instr++ #define PREDICTED_WITH_ARG(op) PRED_##op: oparg = PEEKARG(); next_instr += 3 #define PEEKARG() ((next_instr[2]<<8) + next_instr[1])
在Python中,有些字节码指令通常是成对出现的,更准确说是顺序出现的,这就为上一个字节码指令直接预测下一个字节码指令提供了可能。比如COMPARE_OP的后面就为根据上一个字节码指令直接预测下一个字节码指令提供了可能。COMPARE_OP的后面通常紧跟着JUMP_IF_FALSE或者JUMP_IF_TRUE,这一点我们在demo.py所编译后的字节码可以清楚的看到,而且它们的后面还会跟着一个POP_TOP指令
Python虚拟机中提供了这样的字节码指令预测功能,如果预测成功,就会省去很多无谓的操作,使执行效率得到提升。尤其是当这种字节码指令之间的搭配关系出现的概率非常高时,效率的提升尤为明显
我们可以看到PREDICT(JUMP_IF_FALSE)实际就是直接检查下一条待处理的字节码是否是JUMP_IF_FALSE。如果是,则程序流程会跳转到PRED_JUMP_IF_FALSE标识符对应的代码处,将COMPARE_OP的实现中的PREDICT宏展开,我们就可以看的更清楚了:
ceval.c
if (*next_instr == JUMP_IF_FALSE) goto PRED_JUMP_IF_FALSE; if (*next_instr == JUMP_IF_TRUE) goto PRED_JUMP_IF_TRUE;
那么PRED_JUMP_IF_FALSE和PRED_JUMP_IF_TRUE这些标识符在何处呢?我们知道指令跳跃的目的是为了绕过一些无谓的操作,直接进入JUMP_IF_FALSE或JUMP_IF_TRUE的指令代码,那么显然,这些宏应该位于JUMP_IF_FALSE指令或JUMP_IF_TRUE指令对应的case语句之前
在demo.py中,if a > 10这条判断编译后的字节码序列中,存在JUMP_IF_FALSE指令,那么在COMPARE_OP指令的实现代码的最后,将执行goto PRED_JUMP_IF_FALSE。所以在这里,我们来看一下PRED_JUMP_IF_FALSE标识符是如何被放置到JUMP_IF_FALSE指令代码之前的
ceval.c
PREDICTED_WITH_ARG(JUMP_IF_TRUE); case JUMP_IF_TRUE: //[1]:取出之前的比较操作的结果 w = TOP(); //[2]:如果操作结果为False,进行指令跳跃 if (w == Py_False) { PREDICT(POP_TOP); goto fast_next_opcode; } //[3]:比较操作结果为True if (w == Py_True) { JUMPBY(oparg); goto fast_next_opcode; } err = PyObject_IsTrue(w); if (err > 0) { err = 0; JUMPBY(oparg); } else if (err == 0) ; else break; continue;
我们再看来看PREDICTED_WITH_ARG宏,我们将它展开
PRED_JUMP_IF_TRUE: //取指令的参数 oparg = ((next_instr[2]<<8) + next_instr[1]); //调整next_instr next_instr += 3; case JUMP_IF_TRUE: ……
当程序的执行流程跳转到PRED_JUMP_IF_FALSE处时,首先会通过PEEKARG()从字节码指令序列code取出JUMP_IF_FALSE指令的指令参数,这个指令参数指示了当某个条件满足时Python虚拟机会向前跳跃的字节码指令数。在PEEKARG()之后,Python将字节码指针向前移动了3个字节的长度。
仔细想一下,在COMPARE_OP指令的实现中,PREDICT(JUMP_IF_FALSE)处只是判断下一条字节码是否是JUMP_IF_FALSE,并没有移动next_instr。而接下来的PEEKARG()中,我们获得了JUMP_IF_FALSE的指令参数,也没有移动next_instr,所以这时当确认应该执行JUMP_IF_FALSE时,我们必须将字节码指针移动到JUMP_IF_FALSE之后的下一条字节码,因为这时我们已经开始处理JUMP_IF_FALSE了,而next_instr的使命是指出下一条字节码指令时什么。一个字节码长度是一个字节,而字节码参数都是2个字节的,所以这里需要将next_instr向前移动3个字节
根据前面的分析,执行完COMPARE_OP这条指令后,运行时栈存着的是一个Py_False对象,因此在JUMP_IF_FALSE指令中,要进行跳跃动作:
ceval.c
#define JUMPBY(x) (next_instr += (x))
跳跃的距离就是JUMP_IF_FALSE的指令参数,在这里是9,JUMP_IF_FALSE那一行形象的给出,该指令的结果是跳转到偏移位置27,我们看了下,偏移位置27的指令时POP_TOP指令
………… 2 6 LOAD_NAME 0 (a) 9 LOAD_CONST 1 (10) 12 COMPARE_OP 4 (>) 15 JUMP_IF_FALSE 9 (to 27) 18 POP_TOP 3 19 LOAD_CONST 2 ('a>10') 22 PRINT_ITEM 23 PRINT_NEWLINE 24 JUMP_FORWARD 72 (to 99) >> 27 POP_TOP 4 28 LOAD_NAME 0 (a) 31 LOAD_CONST 3 (-2) 34 COMPARE_OP 1 (<=) 37 JUMP_IF_FALSE 9 (to 49) 40 POP_TOP …………
你可能会疑惑,为什么JUMP_IF_FALSE的参数是9,然后跳转到偏移27的位置,这个27是如何确定的?很简单,我们现在所处的JUMP_IF_FALSE是偏移15,这条指令已经执行完毕了,所以这个跳转参数9是基于下一条指令的偏移位置,下一条的偏移位置是18,所以18+9=27,应该跳到偏移位置27的位置
但为什么是跳转到27 POP_TOP这条指令,而不是偏移位置28的LOAD_NAME指令呢?毕竟第一个if语句比较失败了,那么我们就应该立马开始比较第二个if语句呀!别忘了,运行时栈中还保留着上一次比较结果Py_False,如果我们直接进行下一次比较,而不执行POP_TOP将运行时栈栈顶的Py_False弹出,那么运行时栈会慢慢被比较结果给占满,因此,先执行POP_TOP,再进行再一次比较,是再合理不过的事情了
如果JUMP_IF_FALSE执行时从运行时栈得到的是一个Py_True对象,意味着a > 10成立,接下来就要执行print语句。再次利用PREDICT宏,对POP_TOP指令进行预测,快速进行运行时栈的清理工作,并为print的执行做准备
ceval.c
case POP_TOP: v = POP(); Py_DECREF(v); goto fast_next_opcode;
可能你已经注意到,这里有两个供于预测的宏:PREDICTED_WITH_ARG、PREDICT,正如它们名字所表达的意思,PREDICTED_WITH_ARG是处理有参指令,PREDICT是处理无参指令,POP_TOP正是一条无参指令。实际上,PREDICTED_WITH_ARG和PREDICT目的一致,都是调整next_instr,使其指向下一条待执行的字节码指令
值的注意的是,在执行JUMP_IF_FALSE时,无论运行时栈的比较结果是Py_False,还是Py_True,都将转到fast_next_opcode,执行下一条字节码指令,而不会再执行完JUMP_IF_FALSE之后,如函数调用那般返回COMPARE_OP,再判断下一条操作是否是PREDICT(JUMP_IF_TRUE)
在整个指令跳跃的过程中,出现了两次跳跃,第一次是通过PREDICT(JUMP_IF_FALSE)中的goto语句进行跳跃,这次跳跃影响的是Python虚拟机自身,即实现Python的C代码。而在JUMP_IF_FALSE的指令代码中通过JUMP完成的跳跃是在Python应用程序层面的跳跃,影响的是Python应用程序
在COMPARE_OP中,有一个PREDICT(JUMP_IF_TRUE),但在我们编译后的字节码指令中一直没有JUMP_IF_TRUE这条指令。那么,这条指令会在什么时候出现呢?当我们把if a > 10替换成if not a > 10,Python编译器才会为if语句编译出JUMP_IF_TRUE这条指令
最后还有一点要指出的是,在print的执行中,同样会出现指令跳跃的动作。因为从程序上来看,只要符合一个if的条件,那么后续的if也没有再判断的必要了。所以Python虚拟机的执行流程会一跃千里,直接跳转到if控制结构的末尾的第一条字节码指令,这个跳跃由JUMP_FORWARD完成
case JUMP_FORWARD: JUMPBY(oparg); goto fast_next_opcode;
跳跃的距离是当前字节码指令与if控制结构之后的第一条字节码指令(demo.py中倒数第二条指令"99 LOAD_CONST 8")之间的距离,包括所有字节码指令和其对应的指令参数。所以我们看到,在不同的print语句编译后的指令序列中,JUMP_FORWARD的指令参数是不同的,但它们跳跃的目标却是一致的,都是"99 LOAD_CONST 8"
从这里也可以看到,在JUMP_FORWARD后面的那条POP_TOP对于print语句没有任何意义,它实际上就是为了JUMP_IF_FALSE或JUMP_IF_TRUE而生