终于看完《Python源码剖析》了
2010 10 8 04:06 PM 17784次查看
如果会Python、C和汇编,看懂它应该是不难的。喜欢Python的确实不应错过,一来可以了解Python的实现原理,二来很容易判断怎样的代码性能会更好。
这里就只记录一些自己比较感兴趣,而以前并不太清楚的东西吧。
首先是小整数对象池(small_ints)。
以前就已经知道,Python里的一切都是对象,整数也不例外。而对于众多的整数对象,Python频繁地创建和销毁它们肯定会很影响性能,于是有些小整数对象是不进行销毁的。
不过忘记是从哪篇文章看来的,说这个小整数是指不大于100的自然数;于是我就纳闷了,难道-1这种小负数就不常见么。
好在今天终于弄清了,Python 2.5里小整数的范围是[-5, 257)。
接着是字符串对象的intern机制。
以前和dwing讨论过这个问题,说不可变对象,是否值相同,就应该是同一个对象。
举例来说,a和b这2个变量的值都是'ooxx',那么a和b所引用的对象就必须相同吗?Python、Java和C等大多数语言中都是不一定,而Lua却是一定相同。
dwing向来是Lua控,自然觉得Lua的做法更好,因为节省内存,也更方便比较字符串是否相等,并且从设计上来说,这样处理不可变对象是完全合理的。
然而读了该书,我也就明白为什么Python不这样做了,归根结底还是性能的取舍问题。
Python的字符串拼接比Java的更“先进”,因为Java的2次字符串拼接结果(例如"1"+"2"和""+"12")肯定是2个不同的对象,而Python由于有intern机制,如果值相同的话,是有可能为同一个对象的。
说到这里,你可能会想到,这不就和小整数对象池的原理差不多吗,也是保存常用的一些对象,避免重复创建。当然差别还是有的,小整数对象是一旦创建,直到Python虚拟机运行结束才会销毁,而字符串对象则在没有被引用时,随时都可能被回收的(准确来说也有一类也是不会被回收的),因此再多也不会造成内存泄漏。
既然如此,为什么Python不intern所有的字符串对象呢?书中没有解释,而是阐述了intern机制的原理:它就是用一个PyDictObject对象来做查询,以保证被intern的字符串对象的唯一性的。学过数据结构的应该知道,hash表里的项越多,就越可能引起冲突,于是就要求更大的表空间。
我想,对于Python的实现者来说,对象唯一的优点并不值得浪费这一部分额外的空间,而且势必会造成查询变慢。如果intern所有字符串,那么所有的字符串创建过程都要执行intern操作,而这个操作就需要查表和插入,因此变太慢是不可接受的。
接下来是dict对象的实现。
Python内部大量应用了dict(C层面为PyDictObject)对象,例如用对象名来访问对象及其属性,以及刚才提到的intern机制等。正因如此,dict的性能对Python的整体性能起着至关重要的作用,所以必须采用hash表的设计,而非C++ STL里map所采用的红黑树实现。
有一个值得称道的细节就是PyDictObject对象内部还有个ma_smalltable数组。这个数组只能存放8个对象(的指针),但是对于小的dict来说已经够用了。不够用时,才会自动调用malloc去申请内存空间。也就是说,对于很多条目较少的dict来说,创建它们减少了一次malloc的调用;而对于大dict来说,也不过就浪费了8个对象指针(约32字节)的空间而已。
此外,为了减少创建和销毁的开销,Python也设计了PyDictObject缓冲池,实现原理和数据库连接池差不多。
最后想说的就是Python字节码了。
和Java、C#一样,Python也将源代码编译成了字节码,然后由虚拟机运行字节码。所不同的是Python的字节码更抽象,而Java的字节码更接近于机器语言。(也就是说,Python的一行字节码可能对应多条C代码或函数,而Java的和汇编语言差别不大。)
意外的是,Python提供了dis这个标准模块来进行反汇编,而无需我们自己去解析pyc文件。
用法很简单,对于函数而言,可以直接用dis.dis()来查看:
>>> dis.dis(lambda x: x + 1)
1 0 LOAD_FAST 0 (x)
3 LOAD_CONST 0 (1)
6 BINARY_ADD
7 RETURN_VALUE
有x86汇编经验的应该能猜出这些指令的意思吧,所以我也就不解释了。以前一直觉得Python不会对源码进行任何优化,不过看了字节码后,我改变了这个看法。>>> def a():
... return 1 + 1
...
>>> def b():
... pass
... return 1 + 1
...
>>> dis.dis(a)
2 0 LOAD_CONST 2 (2)
3 RETURN_VALUE
>>> dis.dis(b)
3 0 LOAD_CONST 2 (2)
3 RETURN_VALUE
例如上面这段代码,Python没有老老实实地在运行期去计算1 + 1,而是在编译期就返回常量2了。此外,a和b的字节码居然是一样的,也就是说pass这条语句直接被删除了。
不过Python的编译器也仅能对常量做一些适当的优化,其他变量(包括True)就无能为力了:
>>> def c():
... if True:
... return 1 + 1
...
>>> dis.dis(c)
2 0 LOAD_GLOBAL 0 (True)
3 JUMP_IF_FALSE 8 (to 14)
6 POP_TOP
3 7 LOAD_CONST 2 (2)
10 RETURN_VALUE
11 JUMP_FORWARD 1 (to 15)
>> 14 POP_TOP
>> 15 LOAD_CONST 0 (None)
18 RETURN_VALUE
>>> def d():
... if 1 == 1:
... return 1 + 1
...
>>> dis.dis(d)
2 0 LOAD_CONST 1 (1)
3 LOAD_CONST 1 (1)
6 COMPARE_OP 2 (==)
9 JUMP_IF_FALSE 8 (to 20)
12 POP_TOP
3 13 LOAD_CONST 2 (2)
16 RETURN_VALUE
17 JUMP_FORWARD 1 (to 21)
>> 20 POP_TOP
>> 21 LOAD_CONST 0 (None)
24 RETURN_VALUE
>>> def e():
... if 1:
... return 1 + 1
...
>>> dis.dis(e)
3 0 LOAD_CONST 2 (2)
3 RETURN_VALUE
可以看到,只有“if 1”这个条件判断是直接被优化掉了,其他的“if True”和“if 1 == 1”都只能在运行期判断。同理可得,“while 1”肯定比“while True”快。除了函数,dis()还能查看整个Python文件的编译结果,不过还需要compile()的辅助:
import dis
s = open('ooxx.py').read()
co = compile(s, 'ooxx.py', 'exec')
print dis.dis(co)
之前我还提到过,使用Python内置的运算符,一般会比调用函数快,其实看看字节码就明白了。
例如这个程序:
from operator import pow
a = 123
a ** 456
pow(a, 456)
对应的字节码: 1 0 LOAD_CONST 0 (-1)
3 LOAD_CONST 1 (('pow',))
6 IMPORT_NAME 0 (operator)
9 IMPORT_FROM 1 (pow)
12 STORE_NAME 1 (pow)
15 POP_TOP
3 16 LOAD_CONST 2 (123)
19 STORE_NAME 2 (a)
4 22 LOAD_NAME 2 (a)
25 LOAD_CONST 3 (456)
28 BINARY_POWER
29 POP_TOP
5 30 LOAD_NAME 1 (pow)
33 LOAD_NAME 2 (a)
36 LOAD_CONST 3 (456)
39 CALL_FUNCTION 2
42 POP_TOP
43 LOAD_CONST 4 (None)
46 RETURN_VALUE
None
可以看到,**操作符只需要载入2个参数,然后执行BINARY_POWER即可;pow函数则需要载入函数名和2个参数,然后CALL_FUNCTION。很明显,Python的字节码对应的肯定是C代码,那么BINARY_POWER的性能肯定不会比pow的实现慢。而与此同时,后者还多LOAD_NAME了一次,这个操作实际上是在PyDictObject对象里查找,需要1或多次函数调用,性能肯定不如前者。
其他的我就不提了,感兴趣的自己去看吧~
向下滚动可载入更多评论,或者点这里禁止自动加载。