返回信息流为什么Python程序慢?
Python的慢可是众所周知的。
来一个小程序试试看:
Python:
NUM = 111181111
def is_prime(n):
i = 2
while i < n:
if n % i == 0:
return False
i += 1
return True
print is_prime(NUM)
等效的C程序
#include <stdio.h>
int NUM = 111181111;
int is_prime(int n) {
int i;
for(i = 2; i < n; i++) {
if (n % i == 0) {
return 0;
}
}
return 1;
}
int main() {
int result;
result = is_prime(NUM);
printf("%d\n", result);
return 0;
}
测试一个数是不是质数。我知道111181111是。
然后用Python2.7.6, Jython2.5.3, pypy2.2.1跑Python程序, gcc4.9.0分别用-O0和-O3编译C程序。然后试试速度。
python2 isprime.py 15.60s user 0.01s system 100% cpu 15.609 total
jython isprime.py 8.45s user 0.09s system 117% cpu 7.272 total
pypy isprime.py 1.43s user 0.03s system 99% cpu 1.461 total
./isprime-o0 0.62s user 0.00s system 99% cpu 0.624 total
./isprime-o3 0.60s user 0.00s system 99% cpu 0.605 total
用官方的Python解释器跑,速度比等效的C程序慢30倍?
Jython是把Python翻译成Java字节码然后在JVM上跑,有加速,但比起PyPy来说可以说效果不明显了。
PyPy可以将Python的速度加到C的一半左右。
不公平是吧,仅仅因为使用Python语言,速度就要比C语言慢?
或者,更好的问题是,为什么Python程序慢?
其实这个问题已经有人解答了。推荐来自IBM研究中心的Jose Castanos等人的论文:《On the Benefits and Pitfalls of Extending a Statically Typed Language JIT Compiler for Dynamic Scripting Languages》。文章分析了一个试图将Python运行在一个高性能虚拟机上的事例。他们提到了一些失败的例子和成功的例子,并阐述了成功的关键。
文章的主要观点就是:Python等动态类型语言之所以慢,就是因为每一个简单的操作都需要大量的指令才能完成。他们的虚拟机拥有很强的优化器,却是为静态语言设计的。对Python几乎没有效果。而他们成功的尝试使用了别的技术,下文提到。
举一个例子。对于整数加法,C语言很简单,只要一个机器指令ADD就可以了,最多不过再加一些内存读写。
但是,对于Python来说,a+b这样的简单二元运算,可就真的很麻烦了。Python是动态语言,变量只是对象的引用,变量a和b本身都没有类型,而它们的值有类型。所以,在相“加”之前,必须先判断类型。
1. 判断a是否为整数,否则跳到第9步
2. 判断b是否为整数,否则跳到第9步
3. 将a指向的对象中的整数值读出来
4. 将b指向的对象中的整数值读出来
5. 进行整数相加
6. 生成一个新整数对象
7. 将运算结果存进去
8. 返回这个对象,完成!
9. 判断a是否为字符串,否则跳到第13步
10. 判断b是否为字符串,否则跳到第13步
11. 进行字符串串接操作,生成一个新字符串对象
12. 返回这个对象,完成!
13. 从a的字典里取出__add__方法
14. 调用这个方法,将a和b作为参数传入
15. 返回上述方法的返回值。
这还只是简化版的,实际中还要考虑溢出问题等。
可想而知,如果对于每一次加法运算,C语言只需要一个机器指令,而Python要做这么多操作,Python显然要比C慢得太多。再加上官方的CPython是一个解释器,还要加上每次读指令、指令译码的代价,就更慢了。
Jython只是加快了一些,从上述的例子中,从15秒降到8秒,但是仍然比C慢太多。这是因为Jython能做的只是把Python代码转换成JVM的代码,而Python中那些判断a,b是否为整数或者字符串的操作是不能省略的。毕竟Python是允许你写1+2同时也可以"hello"+"world"。这种运行时的类型检查并不能简单地通过编译而去除。
这里需要提到一个项目:谷歌的Unladen Swallow。它是一个很有雄心的项目,但是,在项目开始一年后就流产了。最后,加速效果也不过50%左右。它们使用的方法是朴素的“模板编译法”:看到Python的加法操作,就转换成一个C语言的函数调用,调用Python的PyNumber_Add函数。这个函数就是干类似上面一串的事。同样地,虽然去除了官方Python的解释器代价,但并没有消除运行时类型检查的代价。
那为什么PyPy能够优化到如此接近C呢?
PyPy使用了一种技巧,就是“类型推导”(Type Inference)。
PyPy的运行时编译器(Just-in-time Compiler,或者称JIT Compiler)的工作方式是,只优化循环,因为大量的时间都是消耗在少数循环上。当运行时检测到某个循环运行的次数很多的时候,就开启一个“录像机”,录制这个循环执行一次中,执行的所有操作的轨迹。这样以“轨迹”为单位的编译方式叫Tracing JIT。
如果给上述判断质数的程序录制“轨迹”,得到的应该是这样:
1. 判断i是整数
2. 判断n是整数
3. 对i和n进行整数“小于”操作
4. 如果不小于,就退出循环
5. 判断i是整数
6. 判断n是整数
7. 计算i除以n的余数
8. 判断上述结果是整数
9. 判断0是整数
10. 比较两者是否相等
11. 如果相等就返回False
12. 判断i是整数
13. 判断1是整数
14. 将i和1进行整数相加,结果赋值给i
15. 回到第1步。
可以显而易见地看出来,这段代码中,充斥着大量的“判断i为整数”和“判断n为整数”这样的操作。但是,很显然,如果i从一开始就是整数,而程序的过程中i从来没有变过,那么i就一直是整数。还有像0、1这样的常数显然是整数。
不仅如此,如果一次循环中i是整数,循环中i只是自加了1,那么下一次循环它还是整数。知道这些知识,优化器就可以大幅度地优化上述代码。
1. 判断i是整数,而且用32位整数装得下,否则跳回解释器执行。
2. 令i0为32位整数,值为i的值
3. 判断n是整数,而且用32位整数装得下,否则跳回解释器执行。
4. 令n0为32位整数,值为n0的值
5. 对i0和n0进行整数“小于”操作
6. 如果不小于,就退出循环
7. 计算i0除以n0的余数
8. 比较上述结果和整数0是否相等
9. 如果相等就返回False
10. 将i0和整数1进行整数相加,结果赋值给i0
11. 但是,如果i0溢出了,就跳回解释器执行。
12. 回到第5步。
看到,原来的循环是1-15,现在只有5-12了。其中涉及的i0和n0还有常数0和1都是32位小整数,都可以直接对应机器指令进行执行。程序起码在这一部分已经由动态的代码变成像C一样的静态类型代码了,而且数据类型很接近机器。将这一段代码编译成机器码,效率就可以和C相比了。
注意到,这其实是一种“猜测”:优化器“猜想”每次执行这段循环,i和n都是整数。这种猜测是可能出错的。万一程序员将一个字符串传入函数怎么办呢?所以,基于“猜测”(speculation)的优化必须考虑“猜错了”的情形。这就是优化过的代码的第1、3、11行的用途。1和3考虑万一i和n不是整数的情形,而15考虑了整数溢出的情况。在Python里,整数都是高精度整数,可以是任意大的,而不仅限于32位。(其实上述32位也只是假设,在64位机上,显然64位效率更高。)所以,如果猜错了(这种事经常会发生),就必须停止执行这段“优化”过的代码,而是老老实实回到解释器中,像传统的Python一样执行。
可以看出,带有类型推导功能的Tracing JIT编译器可以大幅度加快动态语言的速度。主要原因是:
1. 在运行时得到了变量的类型,并通过“猜测”,将这些类型转换成接近机器的类型。
2. 将简化的操作编译成机器码,去除了解释器的代价。
目前,PyPy是一个很活跃的项目。但是,毕竟是一个研究型的项目,PyPy也有自己的不足。如和官方Python并不完全兼容;PyPy本身的可执行文件很大;并不是运行所有的程序都快——PyPy虽然JIT Compiler很快,但它的解释器速度不如官方的Python,对于无法通过优化加速的程序来说,PyPy就不快了。
这是一条镜像帖。来源:北邮人论坛 / python / #68同步于 2014/5/14
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Python机器人发帖
[继续黑Python]为什么Python程序慢?(以及怎么让它快)
nuanyangyang
2014/5/14镜像同步83 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
暂时不懂python。。单纯来顶暖神!
【 在 nuanyangyang (暖羊羊) 的大作中提到: 】
: 为什么Python程序慢?
: Python的慢可是众所周知的。
: 来一个小程序试试看:
: ...................
赞好文
【 在 nuanyangyang (暖羊羊) 的大作中提到: 】
: 为什么Python程序慢?
: Python的慢可是众所周知的。
: 来一个小程序试试看:
: ...................
记得暖身您之前的帖子有提到pypy的“Specialisation”,对这个不甚理解,它的原理是什么呢?这个和类型推导Type Inference有联系吗?求教啦
http://bbs.byr.cn/#!article/Java/26936
【 在 Listjj 的大作中提到: 】
: 记得暖身您之前的帖子有提到pypy的“Specialisation”,对这个不甚理解,它的原理是什么呢?这个和类型推导Type Inference有联系吗?求教啦
: http://bbs.byr.cn/#!article/Java/26936
是一回事。具体地说,就是把更通用的类型(如Python的任意长度int)转换成更特殊的类型(如64位机器整数),使得只要数据不超过这个范围,就会更快。
这么神奇
【 在 nuanyangyang (暖羊羊) 的大作中提到: 】
: 是一回事。具体地说,就是把更通用的类型(如Python的任意长度int)转换成更特殊的类型(如64位机器整数),使得只要数据不超过这个范围,就会更快。
有点眉目了,赞暖神
【 在 nuanyangyang 的大作中提到: 】
:
: 是一回事。具体地说,就是把更通用的类型(如Python的任意长度int)转换成更特殊的类型(如64位机器整数),使得只要数据不超过这个范围,就会更快。