为什么Python程序通常比用C或C++编写的等效程序慢?
为什么Python看起来比C/C++慢呢?我学Python的时候是我的第一门编程语言,但我刚开始学C,就已经感觉到两者之间的明显区别了。
11 个回答
在这里,编译和解释的区别并不重要:Python 是 编译过的,而这只是任何复杂程序运行成本中的一小部分。
主要的成本有:缺少与本地整数对应的整数类型(这使得所有整数操作变得非常昂贵),缺少静态类型(这让方法的解析变得更困难,并且意味着值的类型必须在运行时检查),以及缺少未包装的值(这可以减少内存使用,并避免某种间接访问)。
并不是说这些问题在Python中无法解决或不能提高效率,但选择是为了更方便程序员、灵活性和语言的简洁性,而不是追求运行速度。有些成本可能通过聪明的即时编译(JIT)来克服,但Python所提供的好处总是会带来一些代价。
CPython特别慢,因为它没有即时编译器(JIT)。这是因为它是Python的参考实现,某些情况下更注重简单性而不是性能。Unladen Swallow是一个项目,旨在为CPython添加一个基于LLVM的JIT,从而大幅提升速度。Jython和IronPython可能比CPython快很多,因为它们依赖于经过高度优化的虚拟机(JVM和.NET CLR)。
不过,有一点可以说是让Python变慢的原因,那就是它是动态类型的,每次访问属性时都需要进行大量查找。
举个例子,当你在一个对象A
上调用f
时,可能会在__dict__
中查找,调用__getattr__
等,最后才会调用可调用对象f
的__call__
方法。
关于动态类型,如果你知道自己处理的数据类型,可以进行很多优化。例如在Java或C语言中,如果你有一个整数数组想要求和,最终的汇编代码可以简单到只需获取索引i
的值,加到一个“累加器”上,然后再增加i
。
在Python中,要做到这样的优化非常困难。假设你有一个包含int
的列表子类对象。在添加任何元素之前,Python必须先调用list.__getitem__(i)
,然后通过调用accumulator.__add__(n)
将其加到“累加器”中,然后再重复这个过程。在这里可能会发生很多额外的查找,因为另一个线程可能在调用添加或获取元素之间改变了__getitem__
方法、列表实例的字典或类的字典。即使是在本地命名空间中查找累加器和列表(以及你使用的任何变量)也会导致字典查找。使用任何用户定义的对象时也会有同样的开销,尽管对于某些内置类型,这种情况会有所缓解。
还值得注意的是,像bigint(Python 3中的int,Python 2.x中的long)、列表、集合、字典等原始类型是Python中人们使用得非常多的。这些对象上有很多内置操作已经足够优化。例如,对于上面的例子,你只需调用sum(list)
,而不是使用累加器和索引。坚持使用这些内置类型,以及进行一些整数/浮点数/复数的计算,通常不会有速度问题。如果有,那可能是某个小的时间关键单元(比如SHA2摘要函数),你可以简单地将其移到C(或在Jython中移到Java代码)中。事实上,当你用C或C++编程时,你会浪费大量时间去做一些在Python中只需几秒钟/几行代码就能完成的事情。我认为这种权衡总是值得的,除非你在做嵌入式或实时编程,无法承受这种开销。
Python是一种比C语言更高级的编程语言,这意味着它把计算机的一些复杂细节,比如内存管理和指针等,隐藏起来,让你可以用更接近人类思维的方式来写程序。
确实,如果只看代码执行的速度,C语言的代码通常比Python快10到100倍。但是,如果你把开发时间也考虑进去,Python往往会胜过C。对于很多项目来说,开发时间比运行速度更重要。开发时间越长,直接就意味着成本增加、功能减少和上市时间延长。
Python代码执行较慢的原因在于,它是在运行时解释执行的,而不是在编译时编译成机器代码。
其他一些解释型语言,比如Java字节码和.NET字节码,运行速度比Python快,因为它们的标准版本包含一个即时编译器,可以在运行时把字节码编译成机器代码。CPython没有即时编译器的原因是,Python的动态特性让编写这样的编译器变得困难。目前正在进行一些工作,旨在开发一个更快的Python运行环境,所以未来你可以期待性能差距会缩小,但可能还需要一段时间,标准的Python版本才会包含一个强大的即时编译器。