问题背景: Python在执行的时候会加载每一个模块的PyCodeObject,其中这个对象就包含有opcode,也就是这个模块所有的指令集合,具体定义在源码目录的 /include/opcode.h 中定义了所有的指令集合,在执行的时候通过加载opcode完成指令的流水线执行过程,opcode也就是所有指令集合生成的字符串。执行体位于源码目录的 /Python/ceavl.c 中PyEval_EvalFrameEx()函数就是虚拟机的执行体函数,它会加载指令集合并完成运算。
问题描述:
在PyEval_EvalFrameEx()函数中,同样是通过标准状态机模型完成的指令解析,一个巨大无比的switch结构,类似这样:
在C中,switch语句的执行是逐条对比的,也就是说每一条指令在执行的时候都需要从头对比,因为这里的指令集合是不平均分布的,但是我们可以假设每个指令平均需要匹配n次,n > 1,其实是远远大于1的。
具体问题: 是否可以做优化,为什么作者没有做优化? 如果不采用switch状态机,因为指令码也是有编号的,是否可以直接采用类hashtable的形式来做?
附注: 如果此问题很2请亲提出宝贵的意见
修改方案
做了个测试,基于python 2.7.3,把PyEval_EvalFrameEx这个函数里的case都改成了label,然后利用gcc的labels as values特性,将里面用到的118个opcode与对应的label构成数组:
然后把
switch (opcodes)
改成并逐个替换每个case里的break。
编译后通过了所有的测试(除了test_gdb,这个跟未修改版一样,都是没有sys.pydebug),也就是说这个修改是正确的。
性能测试
接下来的问题是性能……这个该怎么测试呢……没有好的想法,所以随便找了两段代码:
直接loop 5kw次:
修改前运行4次:[4.858, 4.851, 4.877, 4.850],去掉最大的一次,平均4.853s
修改后运行4次:[4.558, 4.546, 4.550, 4.560],去掉最大的一次,平均4.551s
性能提升 (100% - (4.551 / 4.853)) = 6.22%
递归Fibonacci,计算第38个
修改前 [6.227, 6.232, 6.311, 6.241],去掉最大的一次,平均6.233s
修改后 [5.962, 5.945, 6.005, 6.037],去掉最大的一次,平均5.971s
性能提升 (100% - (5.971 / 6.233)) = 4.20%
结论
综合看来,这个小小的改动,的确可以提高5%左右的性能,不知道对各位而言意义有多大……
不过因为用到了只有GCC支持的、非C标准的特性,所以不方便移植。根据StackOverflow的这个帖子,MSVC可以在一定程度上支持,但是貌似很tricky。
不知道这个在Python的发展历程中是否有人做过这样的尝试,也许官方会偏好可移植性?也许抽空可以发个帖子去问问。根据 @方泽图 的说法(见他答案里的评论),Python 3.0+引入了这个优化。详情可以参见他的答案。