BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / python / #68同步于 2014/5/14
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Python机器人发帖

[继续黑Python]为什么Python程序慢?(以及怎么让它快)

nuanyangyang
2014/5/14镜像同步83 回复
为什么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就不快了。
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
taoch机器人#1 · 2014/5/14
暂时不懂python。。单纯来顶暖神! 【 在 nuanyangyang (暖羊羊) 的大作中提到: 】 : 为什么Python程序慢? : Python的慢可是众所周知的。 : 来一个小程序试试看: : ...................
wdjwxh机器人#2 · 2014/5/14
赞好文!
wangxiaobupt机器人#3 · 2014/5/14
赞好文 【 在 nuanyangyang (暖羊羊) 的大作中提到: 】 : 为什么Python程序慢? : Python的慢可是众所周知的。 : 来一个小程序试试看: : ...................
tanoximi机器人#4 · 2014/5/14
赞好文
Listjj机器人#5 · 2014/5/15
赞好文
Listjj机器人#6 · 2014/5/15
记得暖身您之前的帖子有提到pypy的“Specialisation”,对这个不甚理解,它的原理是什么呢?这个和类型推导Type Inference有联系吗?求教啦 http://bbs.byr.cn/#!article/Java/26936
nuanyangyang机器人#7 · 2014/5/15
【 在 Listjj 的大作中提到: 】 : 记得暖身您之前的帖子有提到pypy的“Specialisation”,对这个不甚理解,它的原理是什么呢?这个和类型推导Type Inference有联系吗?求教啦 : http://bbs.byr.cn/#!article/Java/26936 是一回事。具体地说,就是把更通用的类型(如Python的任意长度int)转换成更特殊的类型(如64位机器整数),使得只要数据不超过这个范围,就会更快。
wangxiaobupt机器人#8 · 2014/5/15
这么神奇 【 在 nuanyangyang (暖羊羊) 的大作中提到: 】 : 是一回事。具体地说,就是把更通用的类型(如Python的任意长度int)转换成更特殊的类型(如64位机器整数),使得只要数据不超过这个范围,就会更快。
Listjj机器人#9 · 2014/5/15
有点眉目了,赞暖神 【 在 nuanyangyang 的大作中提到: 】 : : 是一回事。具体地说,就是把更通用的类型(如Python的任意长度int)转换成更特殊的类型(如64位机器整数),使得只要数据不超过这个范围,就会更快。