Python虚拟机中的while循环控制结构
在Python虚拟机之if控制流(一)和Python虚拟机之for循环控制流(二)两个章节中,我们介绍了if和for两个控制结构在Python虚拟机中的实现,但是这里还差一个while循环控制结构。在这里,我们不单单要考虑循环本身的指令跳跃动作,还要考虑另外两个与循环相关的指令跳跃语义:continue和break
下面,让我们看一下demo4.py这个程序,并用dis模块解释其对应的字节码
# cat demo4.py i = 0 while i < 10: i += 1 if i > 5: continue if i == 20: break print(i) # python2.5 …… >>> source = open("demo4.py").read() >>> co = compile(source, "demo4.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (0) 3 STORE_NAME 0 (i) 2 6 SETUP_LOOP 71 (to 80) >> 9 LOAD_NAME 0 (i) 12 LOAD_CONST 1 (10) 15 COMPARE_OP 0 (<) 18 JUMP_IF_FALSE 57 (to 78) 21 POP_TOP 3 22 LOAD_NAME 0 (i) 25 LOAD_CONST 2 (1) 28 INPLACE_ADD 29 STORE_NAME 0 (i) 4 32 LOAD_NAME 0 (i) 35 LOAD_CONST 3 (5) 38 COMPARE_OP 4 (>) 41 JUMP_IF_FALSE 7 (to 51) 44 POP_TOP 5 45 JUMP_ABSOLUTE 9 48 JUMP_FORWARD 1 (to 52) >> 51 POP_TOP 6 >> 52 LOAD_NAME 0 (i) 55 LOAD_CONST 4 (20) 58 COMPARE_OP 2 (==) 61 JUMP_IF_FALSE 5 (to 69) 64 POP_TOP 7 65 BREAK_LOOP 66 JUMP_FORWARD 1 (to 70) >> 69 POP_TOP 8 >> 70 LOAD_NAME 0 (i) 73 PRINT_ITEM 74 PRINT_NEWLINE 75 JUMP_ABSOLUTE 9 >> 78 POP_TOP 79 POP_BLOCK >> 80 LOAD_CONST 5 (None) 83 RETURN_VALUE
在执行"15 COMPARE_OP 0"之前,我们先来看一下运行时栈和名字空间的情况,字节码指令分别在执行"3 STORE_NAME 0"时在名字空间上将符号i和PyIntObject对象0进行映射,在执行"9 LOAD_NAME 0"和"12 LOAD_CONST 1 (10)"分别0和10压入运行时栈:
图1-2执行"15 COMPARE_OP 0"之前运行时栈和名字空间的情况
接着,"15 COMPARE_OP 0"指令将执行<比较操作,并将比较结果压入运行时栈中。这里,我们先来一个思维的跳跃,直接考虑循环结束时的情况,当某个时刻i的值开始大于或等于10,"15 COMPARE_OP 0"指令运行后的运行时栈和local名字空间如图1-2
图1-2循环结束时虚拟机的状态
虚拟机在执行紧接着的"18 JUMP_IF_FALSE 57"时,会发生指令跳跃的动作,跳跃的距离是57到偏移78的位置,根据dis模块的分析结果,这个距离正好是倒数第四条"78 POP_TOP"处,显然这条指令将Py_False弹出运行时栈,随后的虚拟机通过执行POP_BLOCK指令销毁PyTryBlock对象
循环的正常运转
另一方面,如果"15 COMPARE_OP 0"指令处的判断结果为Py_True,那么虚拟机将进入while循环。这里我们不考虑continue和break的情况(实际上break永远不会发生),只考虑循环正常运转时的情况。在循环正常运转时,i的值小于10,且不等于5,那么Python虚拟机接下来的动作及状态转换如图1-3所示:
图1-3循环运转过程中Python虚拟机的状态转换
将i的值递增之后,Python虚拟机通过PRINT_ITEM、PRINT_NEWLINE指令将i值输出到标准输出,然后通过执行指令"75 JUMP_ABSOLUTE 9"进行指令回退,回退的位置是距离字节码开始处的9个字节,正好是"while i < 10:"下面的"9 LOAD_NAME 0"指令。Python虚拟机又开始了新一轮的循环,继续比较i和10的大小,如此反复,在每一次TRUE分支中,都会使i的值递增1,直到i的值等于10。这时程序的执行流程就会转入False分支退出循环,然后Python虚拟机才会执行while循环控制结构之后的字节码指令序列
循环流程改变指令之continue
在demo4.py的while循环中,但i大于或等于5时,意味着"41 JUMP_IF_FALSE 7"指令的判断操作的结果为Py_True,那么这里的指令跳跃动作将不会发生,所以虚拟机将执行接下来的"44 POP_TOP"和"45 JUMP_ABSOLUTE 9"指令。在执行JUMP_ABSOLUTE指令时,虚拟机的流程跳转到"9 LOAD_NAME 0"指令处,这完全符合了continue的语义
循环流程改变指令之break
虽然我们在demo4.py的循环中设置了一条break语句,但实际上这条语句永远不会执行,因为在满足执行break语句的条件时(即i == 20),循环在i大于或等于10时便结束了。虽然break语句无法执行,但我们还是可以根据它执行时动作,看看break是如何跳出当前循环的
虚拟机在执行"61 JUMP_IF_FALSE 5"指令时,如果其中的判断操作的结果为Py_True,就意味着i == 20这个条件满足了,程序流程就进入了对break的执行。Python虚拟机首先会执行"64 POP_TOP"指令,将位于运行时栈栈顶的Py_True弹出,然后执行"65 BREAK_LOOP"指令结束循环
ceval.c
case BREAK_LOOP: why = WHY_BREAK; goto fast_block_end;
Python虚拟机将结束状态why设置为why_break,然后进入fast_block_end。fast_block_end是一段比较复杂的代码块,其中还有关于异常机制的代码,这里我们只截取break相关的代码:
ceval.c
#define JUMPTO(x) (next_instr = first_instr + (x)) fast_block_end: while (why != WHY_NOT && f->f_iblock > 0) { //取得与当前while循环对应的PyTryBlock PyTryBlock *b = PyFrame_BlockPop(f); …… //将运行时栈恢复到while循环之前的状态 while (STACK_LEVEL() > b->b_level) { v = POP(); Py_XDECREF(v); } //处理break语义动作 if (b->b_type == SETUP_LOOP && why == WHY_BREAK) { why = WHY_NOT; JUMPTO(b->b_handler); break; } …… }
Python虚拟机首先获得了之前通过SETUP_LOOP指令申请得到的,与当前while循环对应的PyTryBlock结构,然后根据其中存储的运行时栈信息将运行时栈恢复到while循环之后的状态。最后Python虚拟机将why设置为WHY_NOT,表示退出状态没有任何错误,再通过JUMPTO宏,将虚拟机中下一条指令的指示器next_instr设置为距离code开始位置b->b_handler个字节的指令
这个b_handler是在执行SETUP_LOOP指令时设置的,参考SETUP_LOOP的指令代码和PyFrame_BlockSetup的代码可以看到,这个值会被设置为INSTR_OFFSET() + oparg。注意到这里的oparg是指令"6 SETUP_LOOP 71"的指令参数,即71。INSTR_OFFSET()宏对应的代码为((int)(next_instr - first_instr)),因为在执行SETUP_LOOP指令时,next_instr已经指向了下一条待执行的字节码指令,即"9 LOAD_NAME 0",很显然,这里的b_handler的值为INSTR_OFFSET() + oparg = 9 + 71 = 80。这也意味着break语句将导致Python虚拟机的流程跳转到前面所示的指令序列的倒数第2条指令"80 LOAD_CONST 3"处。确实,它已经跳出while循环所对应的指令序列了
值的注意的是,这里虽然使用了why这个用于栈帧(PyFrameObject)结束时的结束状态,但是实际上并没有结束当前活动的栈帧,而仅仅是利用其实现了break语义。可以看到,最后why又被设置为正常状态的WHY_NOT,而虚拟机仍然在当前栈帧中运行