问题
就一般工作中,我们会考虑将not a in b
改写成a not in b
的写法,但似乎是为什么呢?类似的,还有not a is b
与a is not b
,如果就单单阅读角度考虑的话,确实a not in b
与b is not none
更加的贴近于英语的语法,阅读起来会更加顺畅(但也看人)。但这两种写法,本质上究竟有什么区别呢?下面以not a is none
以及a is not none
举例。
分析及结果
最简单的,把他们的字节码dis出来就知道具体有什么区别了。
1 | import dis |
一目了然,并没有什么实际的区别。所以a is not None
与not a is None
其实怎么用都无所谓,最多就是从阅读方面哪种优于哪种,没了。
思考
到这,貌似也没什么意思,不妨继续仔细再深究下,为什么会是这个结果?明明代码上是不一样,最后出来的字节码却是一摸一样的?相比之下,这个问题似乎更有意思。明显这里肯定是做了一些类似指令优化的东西(下边主要提这个),而指令优化的目的无非就是为了提高速度,进一步,联想到编译优化和运行优化。
优化源码分析
回到一开始,像python之类的编程语言,会先将代码编译成pyc文件,执行的时候,加载该pyc里边一系列的codeObject信息,然后在虚拟机里边跑起来。
而对应codeObject的信息上边也打印出来了,所以最主要的应该是生成pyc的过程,即“编译”中python做了点什么。
有其他语言经验的,其实这里很容易就可以一目了然了。
而”编译”代码的步骤,简单一点的就是生成ast,然后将遍历该ast,生成所谓的字节码。于是我们可以先看看xx is not in xx
和not xx in xx
的ast。
详细一点的呢,可以分词法分析(Lexing),解析(Parsing),编译(Compiling),Iterpreting(解释),但这不是重点,有兴趣的可自行了解ast相关内容。
1 | >>> import ast |
可以看到,ast是不一样的,但字节码又是一样的,确实可能的,就是在这个过程中python又做了些什么,所以呢,可以简单看看python源码中生成字节码的部分。
有兴趣的,可以重点看看compile.c, parsemodule.c,ast.py之类的,也可以结合py的ast文档一起,这个Issue里边也提到了许多相关的内容,就不展开了。
根据上边的ast结果以及文档,可以很简单查到对应着操作符:cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn
下边仅随手贴一点过程中的代码,至于更为详细具体的代码分析生成过程,亦或者一些consts之类的填充,可以看下这个Python int缓存那点事,或者自己慢慢从PyRun_FileExFlags()开撕。不了解字节码的,可以看这个Fun with Python bytecode。
盘下来,解释执行的一些调用关系如下(就展开到代码优化),不想看的,可适当跳过。
1 | PyRun_FileExFlags() |
直接在源码里边搜某些函数的调用时,可能并不能直接搜到,这些函数调用时,都是通过宏展开的,如compile_visit_expr是通过VISIT(c, expr, s->.Assign.value);展开的…
假如我们把PyCode_Optimize()
这一步注释掉,那么出来的字节码是长这样的(看懂这里每一行大概表达的含义,对下边分析优化过程有帮助)
1 | 1 0 LOAD_NAME 0 (a) |
有趣吧,所以所以这究竟是如何从这样子的,转成另外的样子呢?
跳到peephole.c模块中对应的位置。
1 | PyObject * |
整个方法比较复杂,就简单贴关心的内容就好了。通过一个for去遍历整个codestr,遍历的过程是从第一个操作符开始,然后每次去该操作符及其参数所占位置,然后判断做一些优化处理,然后继续跳到下个操作符。
单独的把这个case拿出来分析,还有上边我们看到的字节码。
1 | 1 0 LOAD_NAME 0 (a) |
然后对每个操作符进行分支处理,对于操作符COMPARE_OP,通过if语句,来判断操作数位置,以及下个操作符是否是否为not(这里优化的就是not相关的)或者是基础代码块。如果识别出是可优化内容,即not a is b, not a in b, not a is not b, not a not in b
,那么重新设置一下操作符信息,然后再把哪个not对应的操作符设置为空。相当于就变成了a is not b,a not in b,a is b,a in b
。优化结束,最终字节码就变成了。
1 | 1 0 LOAD_NAME 0 (a) |
最后对整个codestr都执行完优化后,再通过PyCode_New
将所需的内容传递进去,继续去创建对对应的code对象,到这里之后,不妨还可以发现一些其他有趣的优化,如:
1 | /* Replace UNARY_NOT POP_JUMP_IF_FALSE |
编译优化与运行优化
到这,优化就完了,优化的最终目的,还是为了速度上的提升,而这方面,就不仅想到了java,论优化,还是java的优化多得更多。随手贴篇其他人的文章自行阅读,此时,不妨从另外一个角度出发,如果要再进一步的去优化使用的python,还可以从那些角度出发呢?本来想继续往后的,不过看到一篇挺全面,多角度的文章——基于python的opcode优化和模块按需加载机制研究,下面简单罗列一下。
- 因为可以节省编译时间,这里有一篇非常详细的文章,作者在遗传编程领域工作,发现他们Python 程序的总运算时间中,有50%都被编译过程吃掉。于是作者深入到 bytecode 层次进行了小小改动,大幅削减了编译时间,把总的运算时间降至不足原先的一半。(有改进的潜力)
- 优化方向
- 从bytecode入手,对常出现的opcode合并重排
- 从串行转并行入手(多线程、多进程、多核心)
- 优化python的解释逻辑额
- 指令预测等