喜欢上了通过字节码来分析代码差异的感觉,前几天机缘巧合之下玩了下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