喜欢上了通过字节码来分析代码差异的感觉,前几天机缘巧合之下玩了下PHP的opcode,今天来看看Python的bytecode。今天也是巧合,恰好群里有人问Python中3 > 2 == 2为什么结果是True?很多语言其实并没有这种表达式。如果用过JavaScript,就会发现它的结果跟Python不一样,正因为如此,我就对3 > 3 == 2在Python中的底层逻辑有点好奇,那就直接动手吧。
有人猜测3 > 2 == 2可能跟3 > 2 and 2 == 2是一样的,这就对比一下两段代码。跟编译型语言有疑问时就看看编译产生的汇编类似,要知道Python代码的逻辑,就得研究它的字节码。dis模块通过反汇编支持CPython的bytecode分析。
# bc.py
import dis
def foo():
    return 3 > 2 == 2
def bar():
    return 3 > 2 and 2 == 2
dis.dis(foo)
print('-' * 100)
dis.dis(bar)
print('-' * 100)
print(foo(), bar())
$ python3 bc.py
  6           0 LOAD_CONST               1 (3)
              2 LOAD_CONST               2 (2)
              4 DUP_TOP
              6 ROT_THREE
              8 COMPARE_OP               4 (>)
             10 JUMP_IF_FALSE_OR_POP    18
             12 LOAD_CONST               2 (2)
             14 COMPARE_OP               2 (==)
             16 RETURN_VALUE
        >>   18 ROT_TWO
             20 POP_TOP
             22 RETURN_VALUE
----------------------------------------------------------------------------------------------------
 10           0 LOAD_CONST               1 (3)
              2 LOAD_CONST               2 (2)
              4 COMPARE_OP               4 (>)
              6 JUMP_IF_FALSE_OR_POP    14
              8 LOAD_CONST               2 (2)
             10 LOAD_CONST               2 (2)
             12 COMPARE_OP               2 (==)
        >>   14 RETURN_VALUE
----------------------------------------------------------------------------------------------------
True True
光看结果,foo和bar函数的结果都是True,有戏了,可能真的就逻辑一样。分隔线顶部就是3 > 2 == 2的字节码,现在就来分析一下。
先看看上半部分3 > 2 == 2的输出:
- 
    
LOAD_CONST:加载常量3;3 - 
    
LOAD_CONST:加载常量2;2 3 - 
    
DUP_TOP:复制栈顶元素;2 2 3 - 
    
ROT_THREE:旋转栈顶三个元素(栈顶元素旋转后移到栈底,第二个元素移到栈顶,第三个元素移到第二位);2 3 2 - 
    
COMPARE_OP:比较操作符>,栈顶元素作为第二个操作数,第二个元素作为第一个操作数;True 2 - 
    
JUMP_IF_FALSE_OR_POP:判断栈顶元素是否为False,如果是则跳转到地址18,否则弹出栈顶元素;2 - 
    
LOAD_CONST:加载常量2;2 2 - 
    
COMPARE_OP:比较操作符==,栈顶元素作为第二个操作数,第二个元素作为第一个操作数;True - 
    
RETURN_VALUE:返回栈顶元素; - 
    
由于已经
RETURN_VALUE了,后续的字节码就不会执行; 
再分析第二部分的输出:
- 
    
LOAD_CONST:加载常量3;3 - 
    
LOAD_CONST:加载常量2;2 3 - 
    
COMPARE_OP:比较操作符>,栈顶元素作为第二个操作数,第二个元素作为第一个操作数;True - 
    
JUMP_IF_FALSE_OR_POP:判断栈顶元素是否为False,如果是则跳转到地址14,否则弹出栈顶元素; - 
    
LOAD_CONST:加载常量2;2 - 
    
LOAD_CONST:加载常量2;2 2 - 
    
COMPARE_OP:比较操作符==,栈顶元素作为第二个操作数,第二个元素作为第一个操作数;True - 
    
RETURN_VALUE:返回栈顶元素; 
本质上,3 > 2 == 2和3 > 2 and 2 == 2没有什么不同,只不过字节码层面的操作有点差异,Python代码量而言3 > 2 == 2比较简洁,但字节码反而更多,极限情况下,性能稍低,但相差不多,以执行10亿次为例,才相差2秒左右。
# cmp.py
import timeit
number = 1000000000
print(timeit.timeit('3 > 2 == 2', number=number))
print(timeit.timeit('3 > 2 and 2 == 2', number=number))
$ python3 cmp.py
17.322567800000797
14.951357840000128
如果用jit版本的python3.13(目前还是实验版,需要自行编译才能启用jit),差异就更小,也就0.5秒。
$ ./python3.13jit/cpython-3.13.1/output/bin/python3 cmp.py
9.997729628999878
9.506675130000076