高性能python(1)必须知道的python短板
现在电脑一般都是多核,总体计算能力提高,无须等待内存障碍让单个核心可以跑得更快。
但是给CPU增加更多核心并不一定能提升程序运行的速度,阿姆达尔定律认为:如果一个可以运行在多核上的程序有某些执行路径必须运行在单核上,那么这些路径就会成为瓶颈导致最终速度无法通过增加更多核心来提高。
比如,假如东京热要拍年度影片,这个影片需要100个女忧参与,一个女优在影片中出镜时长为3分钟,如果只有一个男优参与该影片并且每次出镜只能有一个女优,不考虑演员身体的折损的理想情况下,东京热要拍完这部影片至少需要100*3分钟,因为女优就像队列一样,男优只有与一名女优合作结束之后才能与下一名女优拍戏,而女优也必须等待前面的女优拍完才能轮到她。
如果东京热有两名男优,那么影片完成时间就可以减少一半,因为两位男优是独立完成工作没有依赖关系。为了继续提速,如果有100名男优,那么影片只需要3分钟就可以完成,也就是单人完成任务的极限时间,如果用超过100人数的男优,效率也不会再增加,只能提高单人的执行效率来提高。
对于CPU也是一样,我们可以增加更多的核心直到某个必须单核执行的任务成为瓶颈,也就是说任何并行计算的瓶颈最终都会落到在其顺序执行的那部分上。
另外,对于python而言,充分利用多核性能的阻碍主要在于python的全局解释器锁(GIL)。GIL确保python进程一次只能执行一条指令,无论当前有多少个核心。这意味着即使某些python代码可以使用多个核心,在任意时间点仅有一个核心在执行python的指令。用东京热的例子来说,有100个女优而只有一个男优,这是一个严重的效率阻碍,特别是在计算机发展拥有更多而非更快的计算单元的时候,因为单个计算单元已经到了物理级限制,也就是晶体管到达物理极限了,也只能通过架构和集群来提高运算速度。
尽管python在这方面有重大阻碍,但也是有解决办法的比如标准库的multiprocessing,numexpr,Cython和分布式模型等。
大多数问题似乎都证明python并不适合解决性能问题,但这不是真的,原因有两点,首先所有这些“高性能要素”中,我们忽略一个至关重要的:开发者。另外,高性能python可以通过各种模块(比如用numpy处理矩阵)和原理来帮助减轻遇到的高性能问题。
理想计算模型和python虚拟机:
先看一段判断质数的代码:
import math
def check_prime(number):
__sqrt_number=math.sqrt(number)
__number_float=float(number)
__for i in range(2,int(sqrt_number)+1):
____if (number_float/i).is_integer():
______return False
__return True
在代码的开头,我们将number的值保存于RAM中。为了计算sqrt_number和number_float,我们需要将该值存入CPU。理想情况下我们只需要传一次,它被保存在CPU的L1/L2缓存中,然后CPU会进行两次计算并将结果传回RAM保存。这是一个理想情况,因为我们令从RAM中读取number的值的次数最少,转而以快很多的L1/L2缓存的读取代替。将数据保持在需要的地方并尽量少移动这一场景对优化来说至关重要。所谓“沉重数据”的概念指的是移动数据需要花费时间,而这就是我们需要避免的。
CPU是支持矢量计算的,且我们也不需要上面代码返回的所有数据,我们只需要一个结果,我们可以让程序一次对多个i值进行除法而不是对每一个i进行独立操作。
python虚拟机:
python解释器为了抽离底层用到的计算元素做了很多工作。让编程人员无须考虑如何分配内存,如何组织内存以及用什么样的顺序将内存传入CPU,这是python的优势,让你能够集中在算法的实现上,然而它有个巨大的性能代价。
举两个例子:
def search_fast(haystack,needle):
__for item in haystack:
____if item==needle:
______return True
__return False
def search_slow(haystack,needle):
__return_value=False
__for item in haystack:
____if item==needle:
______return_value= True
__return return_value
虽然上面都是复杂度相同的程序,但是执行效率不一样的,因为search_fast版本提前结束了循环,减少不必要的计算。
Python虚拟机抽象层的影响之一就是矢量操作变得不是直接可用了。我们最初的质数函数会循环遍历i的每一个值而不是将多次遍历组合成一个矢量操作。而我们抽象以后的矢量代码并不是合法的python代码,因为我们不能用一个列表去除一个浮点,不过我们有numpy这样的外部库可以通过增加矢量化的数学操作。
另外,python抽象还影响了任何需要为下一次计算保存L1/L2缓存中的相关数据的优化。这有很多原因,首先是python对象不再是内存中最优化的布局。这是因为python是一种垃圾收集语言,内存会被自动分配并在需要的时候释放,这会导致内存碎片并影响向CPU缓存传输。另外我们也没有机会去直接改变数据结构在内存的布局,这就意味着就算信息少于总线带宽,总线上的一次传输可能并不包含一次计算的所有相关信息。
还有一个问题更基本:
python并不是一种编译性的语言而且还是动态的,这就是说任何算法上可能的优化机会都会更难实现,因为代码有可能在运行时被改变,解决这个问题主要还是用Cython。