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
7import 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 | 一次打印多个进度符号: |