所谓编程说的简单一点就是为了达到某个目的,对比特进行传输和加工。而高性能编程就是以尽可能小的代价来传输和加工比特。在弄清楚如何在Python中实现高性能编程之前,弄清楚比特如何在真实的计算机系统中移动和加工很有必要。
计算机系统可以简单划分为三个部分:计算单元,存储单元以及他们之间的连接单元。
计算单元
对于计算单元,我们最感兴趣的是它在一个时钟周期里面可以执行多少次运算以及一秒钟包含多少个时钟周期。前者用IPC(Instructions Per Cycle)来衡量,后者用时钟频率来衡量。因为硬件的限制,IPC和时钟频率已经无法单方面继续大幅提升了,为了加快运算速度,主要通过其他的方法来达到:超线程,乱序执行以及现在非常流行的多核架构。经常有人问,是不是核数越多程序跑的越快,答案是否定的,Amdahl’s law给出了完整的阐述,简单说就是,程序中只能串行执行的比例越低且处理器越多,加速比越高,程序效率越高。也就是说,即使到了多核时代,提升程序的并发度仍然具有十分重要的意义。
存储单元
对于存储单元,读写速度和延迟是两个主要性能指标。其中读写速度除了跟存储介质有关,还跟数据读取方式有直接关系,比如顺序读肯定随机读要快的多。从成本方面考虑,现代的存储体系都是分层的,所有数据都是存储在硬盘上,其中一部分会被加载到RAM中,更少一部分会被加载到L1/L2 cache。因此在优化程序的存储模型时,需要考虑这个数据存储在哪里,如何组织以及它会被移动几次。异步I/O和预先式缓存常用来加速访问速度,因为都不需要等待数据获取这个耗时的操作。
连接单元
连接单元有很多种变形,不过都可以统称为总线。比如后端总线(连接L1/L2 cache和CPU)、前端总线(连接RAM和L1/L2 cache)、外部总线(连接CPU内存和硬盘网卡)。对于连接单元,有两个主要性能指标:一次传输的数据量(总线带宽)和一分钟传输多少次(总线频率)。其中大总线带宽利于顺序读应用(一次大量),而高总线频率则利于随机读应用(多次少量)。
在了解了计算机系统的三大基础组件之后,可以从中了解到高性能编程的三个基本原则:
- 充分利用多核CPU,尽可能的并行处理
- 尽量把数据放在它被需要的地方
- 尽可能少的移动数据
而对于Python语言来讲,Python的解释器在底层计算资源的抽象上做了很多的工作。开发者完全不需要关心如何为数组分配内存,如何组织内存以及它如何被传送到CPU,从而让开发者只需要关注业务功能的实现,所以Python上手快,开发效率高,但这些都是以Python运行效率低为代价的。
Python运行效率低的几个原因:
- Python无法很容易利用CPU的向量化特性(单指令多数据流SIMD),numpy包可以支持。
- 因为Python是垃圾回收语言,在内存中必然会产生内存碎片,这对数据传输和存储(特别是在L1/L2 Cache这个层面)都是不高效的
- Python的支持动态类型的非编译语言,数据类型只有在运行时才能确定,解释器无法提前对代码进行优化
- Python的GIL会限制程序利用现在无处不在的多核CPU