Python 加速计算介绍
Python 编写程序简单高效,但运行效率相比 C 等较慢,不太适合处理计算密集型任务(对 IO 较适用)。如果想要适用 Python 进行密集计算,可以采用某些手段加速计算,一定程度上缓解这种矛盾。
Numba 模块
Numba is an open source JIT compiler that translates a subset of Python and NumPy code into fast machine code.
Numba 是开源的 JIT 编译器,它通过 llvmlite Python 包,使用 LLVM 将 Python 的子集和 NumPy 翻译成快速的机器码。它为在 CPU 和 GPU 上并行化 Python 代码提供了大量选项,而经常只需要微小的代码变更。下面给出一个实例介绍 Numba 模块加速计算效果。
| 1 | import timeit | 
输出结果如下:
| 1 | 内置函数优化耗时: 0.9235639999969862s | 
使用 Python 内置函数,减少循环能够一定程度上降低运行速度;使用加速模块 numba 在数值循环上能够大大降低运行速度。
Multiprocessing 模块
multiprocessing 包同时提供了本地和远程并发操作,通过使用子进程而非线程有效地绕过了 全局解释器锁。 因此,multiprocessing 模块允许程序员充分利用给定机器上的多个处理器。 它在 Unix 和 Windows 上均可运行。
一般使用方法:
| 1 | import os | 
结果如下:
| 1 | 总共用时 4.6657445430755615 秒 | 
不使用多进程时:
| 1 | def main(s): | 
结果如下:
| 1 | my_sum1 | 
可见多进程确实降低了时间。
当调用函数相同,参数变化时,可以使用如下方法:
| 1 | import time | 
结果如下:
| 1 | 总共用时 0.3504462242126465 秒 | 
可以看出,结果比较混乱,而且计时不对,这是因为采用 map_async 异步非阻塞方式进行的。下面是非异步阻塞式:
| 1 | import time | 
结果如下:
| 1 | my_sum1my_sum2my_sum3 | 
无论采用哪种多进程模式,都能够降低运行时间。
还有一种多进程使用方法:
| 1 | import os | 
结果如下:
| 1 | my_sum2my_sum1my_sum3 | 
上面同样属于异步非阻塞方法,非异步方法如下:
| 1 | import os | 
结果如下:
| 1 | my_sum1 | 
关于 map, map_async, apply, apply_async 的区别,如下表:
| 1 | Multi-args Concurrence Blocking Ordered-results | 
可以根据需要选择相应的多进程方法。
关于异步非阻塞式和非异步阻塞式:
- 异步非阻塞式是主进程首先运行,碰到子进程不切换,当操作系统进行切换时,再运行子进程。如上面的 async 式的,主进程将首先遍历完整个代码,打印时间,切换后,运行子进程 p.map_async(main, [my_sum1, my_sum2, my_sum3]) 中的 main(my_sum1) 等。 
- 非异步阻塞式是首先主进程开始运行,碰到子进程,操作系统切换到子进程,等待子进程运行结束后,再切换到另外一个子进程,直到所有子进程运行完毕。然后再切换到主进程,运行剩余的部分。 
一个例子
求 2 ~ 250001 中所有素数的个数。
使用 C 语言编程
编程 C 程序,保存为 primes.c
| 1 | 
 | 
编译运行
| 1 | # 编译 | 
结果如下,耗时 8秒多
| 1 | $ time ./primes | 
下面,使用python程序测试运行时间。
编写第一个Python程序
编程 python 程序,保存为 primes.py
| 1 | def isPrime(n): | 
不需要编译,直接运行。结果耗时111秒多
| 1 | $ time python primes.py | 
编写第二个Python程序
使用numba加速。编程 python 程序,保存为 primesNumbaSpeed.py
| 1 | from numba import jit | 
不需要编译,直接运行。结果耗时10秒多,比上一版快了10倍多
| 1 | $ time python primesNumbaSpeed.py | 
编写第三个Python程序
使用numba 和 multiprocessing 加速。编程 python 程序,保存为 primesNumbaSpeed.py
| 1 | from multiprocessing import Pool | 
不需要编译,直接运行。显示耗时15 秒多。因为使用多进程,时间显示不准确,实际耗时只有1.4 秒左右。
| 1 | $ time python primeMultiprocessSpeed.py | 
在 notebook 中调用打印真实时间
| 1 | from primeMultiprocessSpeed import sumPrime, partition | 
| 1 | %%timeit | 
时间消耗 1.4 秒左右,比 C 语言写的代码还要快 3 倍。
| 1 | 22044 | 
因此,当使用 python 进行快速开发时,如果需要优化运行速度,建议使用 numba 和 multiprocessing 进行加速。
使用 pyinstrument 发现 python 程序中执行耗时的部分
首先安装包
| 1 | pip install pyinstrument | 
使用方法:
- 在 终端 中运行 - 1 - /usr/local/miniconda/bin/pyinstrument pyscripts/primeMultiprocessSpeed.py 
- 在 jupyter 中运行 - 1 
 2
 3
 4
 5
 6
 7- import pyinstrument 
 profiler = pyinstrument.Profiler()
 with profiler:
 # 需要测试的代码
 print(profiler.output_text())
结果如下
| 1 | 22044 | 
从每行的前面的运行时间,可以看到程序耗时的地方在哪里。
与 tqdm 搭配打印多进程程序运行时进度条
编写类似如下代码,命名为 main.py
| 1 | from multiprocessing import Pool | 
然后在终端运行如下命令:
| 1 | python main.py | 
得到如下信息:
| 1 | $ python pbar.py | 
注意:
- 代码 main.py中for循环可以替换为任意类型循环,如:- for i in range(N):
- for i in enumerate(range(N)):等等。
 
- callback必须写成函数形式:- update;
- myfunc函数必须在全局范围内可以让- pool.apply_async查到;
- error_callback可写可不写,建议写。
自制进度条
进度条除了使用包 tqdm 外,还可以自己实现,编写代码 tools.py:
| 1 | import time | 
在终端运行:
| 1 | python tools.py | 
输出结果如下:
| 1 | 一次打印多个进度符号: | 
将 pbar 中的 > 换成 chr(9608),即:
| 1 | pbar = f"\r[{step:3d}%] {chr(9608) * step}".ljust(108, '.') + f"(Elapsed:{current_time - start_time:.2f}s)" | 
打印结果将变为:
| 1 | 一次打印多个进度符号: | 










