乐趣区

关于python:深入理解-python-虚拟机字节码教程3深入剖析循环实现原理

深刻了解 python 虚拟机:字节码教程 (3)——深刻分析循环实现原理

在本篇文章当中次要给大家介绍 cpython 当中跟循环相干的字节码,这部分字节码相比起其余字节码来说绝对简单一点,通过剖析这部分字节码咱们对程序的执行过程将会有更加粗浅的了解。

循环

一般 for 循环实现原理

咱们应用各种例子来了解和循环相干的字节码:

def test_loop():
    for i in range(10):
        print(i)

下面的代码对应的字节码如下所示:

  8           0 LOAD_GLOBAL              0 (range)
              2 LOAD_CONST               1 (10)
              4 CALL_FUNCTION            1
              6 GET_ITER
        >>    8 FOR_ITER                12 (to 22)
             10 STORE_FAST               0 (i)

  9          12 LOAD_GLOBAL              1 (print)
             14 LOAD_FAST                0 (i)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_ABSOLUTE            8
        >>   22 LOAD_CONST               0 (None)
             24 RETURN_VALUE

首先是 range 他对应一个 builtin 的类型,在执行下面的字节码的过程当中,首先先将 range 将在进入栈空间当中,而后将常量 10 加载进入栈空间当中,最初会调用指令 CALL_FUNCTION,这个时候会将栈顶的两个元素弹出,调用 range 类型的创立函数,这个函数会返回一个 range 的实例对象。

这个时候栈的后果如下所示:

接下来的一条字节码为 GET_ITER,这条字节码的含意为,弹出栈顶的对象,并且将弹出的对象变成一个迭代器,并且将失去的迭代器对象再压入栈空间当中。

接下来的一条指令是 FOR_ITER,这条指令的含意为:已知栈顶对象是一个迭代器,调用这个迭代器的 \_\_next\_\_ 函数:

  • 如果迭代器曾经迭代实现了,则将栈顶的迭代器弹出,并且将 bytecode 的 counter 加上对应的参数值,在下面的函数字节码当中这个参数值等于 12,也就是说下一条指令为字节码序列的 22 这个地位。
  • 如果没有迭代实现则将函数的返回值压入栈顶,并且不须要弹出迭代器,比方当咱们第一次调用 \_\_next\_\_ 函数的时候,range 的返回值为 0,那么此时栈空间的内容如下所示:

接下来执行的字节码为 STORE_FAST,这条字节码的含意就是弹出栈顶的元素,并且将这个元素保留到 co_varnames[var_num] 当中,var_num 就是这条字节码的参数,在下面的函数当中就是 0,对应的变量为 i,因而这条字节码的含意就是弹出栈顶的元素并且保留到变量 i 当中。

LOAD_GLOBAL,将内嵌函数 print 加载进入栈中,LOAD_FAST 将变量 i 加载进入栈空间当中,此时栈空间的内容如下所示:

CALL_FUNCTION 会调用 print 函数打印数字 0,并且将函数的返回值压入栈空间当中,print 函数的返回值为 None,此时栈空间的内容如下所示:

POP_TOP 将栈顶的元素弹出,JUMP_ABSOLUTE 字节码有一个参数,在下面的函数当中这个参数为 8,当执行这条字节码的时候间接将 bytecode 的 counter 间接设置成这个参数值,因而执行完这条字节码之后下一条被执行的字节码又是 FOR_ITER,这便实现了循环的成果。

综合剖析下面的剖析过程,实现循环的成果次要是有两个字节码实现的,一个是 FOR_ITER,当迭代器迭代实现之后,会间接跳出循环,实现这个的原理是在字节码的 counter 上加上一个值,另外一个就是 JUMP_ABSOLUTE,他能够间接跳到某一处的字节码地位进行执行。

continue 关键字实现原理

def test_continue():
    for i in range(10):
        data = random.randint(0, 10)
        if data < 5:
            continue
        print(f"{data =}")

其实通过对下面的字节码的剖析之后,咱们能够大抵剖析出 continue 的实现原理,首先咱们晓得 continue 的语意间接进行下一次循环,这个语意其实和循环体执行完之后的语意是一样的,在上一份代码的剖析当中实现这个语意的字节码是 JUMP_ABSOLUTE,间接跳到 FOR_ITER 指令的地位持续开始执行。咱们当初来看看下面的函数对应的字节码是什么:

 13           0 LOAD_GLOBAL              0 (range)
              2 LOAD_CONST               1 (10)
              4 CALL_FUNCTION            1
              6 GET_ITER
        >>    8 FOR_ITER                40 (to 50)
             10 STORE_FAST               0 (i)

 14          12 LOAD_GLOBAL              1 (random)
             14 LOAD_METHOD              2 (randint)
             16 LOAD_CONST               2 (0)
             18 LOAD_CONST               1 (10)
             20 CALL_METHOD              2
             22 STORE_FAST               1 (data)

 15          24 LOAD_FAST                1 (data)
             26 LOAD_CONST               3 (5)
             28 COMPARE_OP               0 (<)
             30 POP_JUMP_IF_FALSE       34

 16          32 JUMP_ABSOLUTE            8

 17     >>   34 LOAD_GLOBAL              3 (print)
             36 LOAD_CONST               4 ('data =')
             38 LOAD_FAST                1 (data)
             40 FORMAT_VALUE             2 (repr)
             42 BUILD_STRING             2
             44 CALL_FUNCTION            1
             46 POP_TOP
             48 JUMP_ABSOLUTE            8
        >>   50 LOAD_CONST               0 (None)
             52 RETURN_VALUE
  • LOAD_GLOBAL 0 (range): 加载全局变量 range,将其压入栈顶。
  • LOAD_CONST 1 (10): 加载常量值 10,将其压入栈顶。
  • CALL_FUNCTION 1: 调用栈顶的函数,此处为 range 函数,并传入一个参数,参数个数为 1。
  • GET_ITER: 获取迭代器对象。
  • FOR_ITER 40 (to 50): 迭代循环的开始,当迭代实现之后将字节码的 counter 加上 40,也就是跳转到 50 的地位执行。
  • STORE_FAST 0 (i): 将迭代器的值存储到局部变量 i 中。
  • LOAD_GLOBAL 1 (random): 加载全局变量 random,将其压入栈顶。
  • LOAD_METHOD 2 (randint): 加载对象 random 的属性 randint,将其压入栈顶。
  • LOAD_CONST 2 (0): 加载常量值 0,将其压入栈顶。
  • LOAD_CONST 1 (10): 加载常量值 10,将其压入栈顶。
  • CALL_METHOD 2: 调用栈顶的办法,此处为 random.randint 办法,并传入两个参数,参数个数为 2。
  • STORE_FAST 1 (data): 将办法返回值存储到局部变量 data 中。
  • LOAD_FAST 1 (data): 加载局部变量 data,将其压入栈顶。
  • LOAD_CONST 3 (5): 加载常量值 5,将其压入栈顶。
  • COMPARE_OP 0 (<): 执行比拟操作 <,将后果压入栈顶。
  • POP_JUMP_IF_FALSE 34: 如果栈顶的比拟后果为假,则跳转到字节码偏移为 34 的地位。
  • JUMP_ABSOLUTE 8: 无条件跳转到字节码偏移为 8 的地位,即循环的下一次迭代。
  • LOAD_GLOBAL 3 (print): 加载全局变量 print,将其压入栈顶。
  • LOAD_CONST 4 (‘data = ‘): 加载常量字符串 ‘data = ‘,将其压入栈顶。
  • LOAD_FAST 1 (data): 加载局部变量 data,将其压入栈顶。
  • FORMAT_VALUE 2 (repr): 格式化栈顶的值,并指定格式化形式为 repr。
  • BUILD_STRING 2: 构建字符串对象,蕴含两个格式化后的值。
  • CALL_FUNCTION 1: 调用栈顶的函数,此处为 print 函数,并传入一个参数,参数个数为 1。
  • POP_TOP: 弹出栈顶的值,也就是函数 print 的返回值,print 函数返回值为 None。
  • JUMP_ABSOLUTE 8: 无条件跳转到字节码偏移为 8 的地位,即循环的下一次迭代。
  • LOAD_CONST 0 (None): 加载常量值 None,将其压入栈顶。
  • RETURN_VALUE: 返回栈顶的值,即 None。

这段字节码实现了一个简略的循环,应用 range 函数生成一个迭代器,而后对迭代器进行遍历,每次遍历都会调用 random.randint 办法生成一个随机数并存储到局部变量 data 中,而后依据 data 的值进行条件判断,如果小于 5 则打印 “data = ” 和 data 的值,否则持续下一次循环,直到迭代器完结。最初返回 None。

break 关键字实现原理

def test_break():
    for i in range(10):
        data = random.randint(0, 10)
        if data < 5:
            break
    return "BREAK"

下面的函数对应的字节码如下所示:

 21           0 LOAD_GLOBAL              0 (range)
              2 LOAD_CONST               1 (10)
              4 CALL_FUNCTION            1
              6 GET_ITER
        >>    8 FOR_ITER                28 (to 38)
             10 STORE_FAST               0 (i)

 22          12 LOAD_GLOBAL              1 (random)
             14 LOAD_METHOD              2 (randint)
             16 LOAD_CONST               2 (0)
             18 LOAD_CONST               1 (10)
             20 CALL_METHOD              2
             22 STORE_FAST               1 (data)

 23          24 LOAD_FAST                1 (data)
             26 LOAD_CONST               3 (5)
             28 COMPARE_OP               0 (<)
             30 POP_JUMP_IF_FALSE        8

 24          32 POP_TOP
             34 JUMP_ABSOLUTE           38
             36 JUMP_ABSOLUTE            8

 26     >>   38 LOAD_CONST               4 ('BREAK')
             40 RETURN_VALUE

这段字节码与之前的字节码类似,但有一些轻微的不同。

  • LOAD_GLOBAL 0 (range): 加载全局变量 range,将其压入栈顶。
  • LOAD_CONST 1 (10): 加载常量值 10,将其压入栈顶。
  • CALL_FUNCTION 1: 调用函数,函数参数个数为 1。
  • GET_ITER: 从栈顶获取可迭代对象,并返回迭代器对象。
  • FOR_ITER 28 (to 38): 遍历迭代器,如果迭代器为空,则跳转到字节码偏移为 38 的地位,即跳出循环,否则继续执行下一条字节码。
  • STORE_FAST 0 (i): 将迭代器的以后值存储到局部变量 i 中。

接下来的字节码与之前的字节码类似,都是调用 random.randint 办法生成随机数,并将随机数存储到局部变量 data 中。而后,对局部变量 data 进行条件判断,如果小于 5 则跳出循环,否则持续下一次循环。不同的是,这里应用了 POP_TOP 操作来弹出栈顶的值,即格式化后的字符串,无需应用。

  • POP_JUMP_IF_FALSE 8: 如果栈顶的值(即 data)不满足条件(小于 5),则跳转到字节码偏移为 8 的地位,即循环的下一次迭代。
  • POP_TOP: 弹出栈顶的值,也就是将迭代器弹出。
  • JUMP_ABSOLUTE 38: 无条件跳转到字节码偏移为 38 的地位,即跳出循环。
  • JUMP_ABSOLUTE 8: 无条件跳转到字节码偏移为 8 的地位,即循环的下一次迭代。

最初,字节码加载了一个常量字符串 ‘BREAK’,并通过 RETURN_VALUE 操作将其作为返回值返回。这段字节码实现了相似于之前的循环,但在满足条件时应用了 POP_TOP 和跳转指令来优化循环的执行。

从下面的剖析过程能够看进去 break 的实现也是通过 JUMP_ABSOLUTE 来做到的,间接跳转到循环内部的下一行代码。

总结

在本本篇文章当中次要给大家剖析了在 python 当中也循环无关的字节码,实现循环操作的次要是几个外围的字节码 FOR_ITER,JUMP_ABSOLUTE,GET_ITER 等等。只有深刻理解了这几个字节码的性能了解循环的过程就很简略了。


本篇文章是深刻了解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

退出移动版