雷锋网 AI 科技评论按,本文是工程师 Jim Anderson 分享的关于「经过并发性加快 python 程序的速度」的文案的第三部分,重点内容是 CPU 绑定程序加速关联。
在前面两篇中,咱们已然讲过了关联的概念以及I/O 绑定程序的加速,这篇是这一系列文案的最后一篇,讲的是 CPU 程序加速。雷锋网 AI 科技评论编译整理如下:
怎样加速 CPU 绑定程序
到日前为止,前面的例子都处理了一个 I/O 绑定问题。此刻,你将科研 CPU 绑定的问题。如你所见,I/O 绑定的问题大部分时间都在等待外边操作(如网络调用)完成。另一方面,CPU 限制的问题只执行很少的 I/O 操作,它的总体执行时间取决于它处理所需数据的速度。
在咱们的示例中,咱们将运用一个有点愚蠢的函数来创建有些必须在 CPU 上运行很长期的东西。此函数计算从 0 到传入值的每一个数字的平方和:
你将处理一大批数据,因此这必须一段时间。记住,这只是代码的一个占位符,它实质上做了有些有用的事情,必须海量的处理时间,例如计算公式的根或对大型数据结构进行排序。
CPU 绑定的同步版本
此刻让咱们看一下这个示例的非并发版本:
import time
def cpu_bound(number): return sum(i * i for i in range(number))
def find_sums(numbers):
for number in numbers:
cpu_bound(number)
if __name__ == "__main__":
numbers = [5_000_000 + x for x in range(20)]
start_time = time.time
find_sums(numbers)
duration = time.time - start_time
print(f"Duration {duration} seconds")
此代码调用 cpu_bound 20 次,每次运用区别的大数字。它在单个 CPU 上单个进程中的单个线程上完成所有这些工作。执行时序图如下:
与 I/O 绑定示例区别,CPU 绑定示例的运行时间一般相当一致。这台设备大约必须 7.8 秒:
显然咱们能够做得更好。这都是在无并发性的单个 CPU 上运行的。让咱们瞧瞧咱们能做些什么来改善它。
线程和异步版本
你认为运用线程或异步重写此代码会加快速度吗?
倘若你回答「一点亦不」,这是有道理的。倘若你回答,「它会减慢速度,」那就更对啦。
原由如下:在上面的 I/O 绑定示例中,大部分时间都花在等待缓慢的操作完成上。线程和异步经过准许你重叠等待的时间而不是按次序执行,这能加快速度。
然则,在 CPU 绑定的问题上,不必须等待。CPU 会尽可能快速地起步以处理问题。在 python 中,线程和任务都在同一进程中的同一个 CPU 上运行。这寓意着一个 CPU 不仅做了非并发代码的所有工作,还必须做线程或任务的额外工作。它花费的时间超过 10 秒:
我已然编写了这个代码的线程版本,并将它与其他示例代码放在 Github repo 中,这般你就能够自己测试它了。
CPU 绑定的多处理版本
此刻,你最终要接触多处理真正与众区别的地区啦。与其他并发库区别,多处理被显式设计为跨多个 CPU 一起承担工作负载。它的执行时序图如下所示:
它的代码是这般的:
import multiprocessing
import time
def cpu_bound(number): return sum(i * i for i in range(number))
def find_sums(numbers):
with multiprocessing.Pool as pool:
pool.map(cpu_bound, numbers)
if __name__ == "__main__":
numbers = [5_000_000 + x for x in range(20)]
start_time = time.time
find_sums(numbers)
duration = time.time - start_time
print(f"Duration {duration} seconds")
这些代码和非并发版本相比几乎无要更改的。你必要导入多处理,而后把数字循环改为创建多处理.pool 对象,并运用其.map办法在工作进程空闲时将单个数字发送给它们。
这正是你为 I/O 绑定的多处理代码所做的,然则这儿你不必须担心会话对象。
如上所述,处理 multiprocessing.pool构造函数的可选参数值得重视。能够指定要在池中创建和管理的进程对象的数量。默认状况下,它将确定机器中有多少 CPU,并为每一个 CPU 创建一个进程。虽然这针对咱们的简单示例来讲特别有用,但你可能期盼在生产环境它亦能发挥功效。
另一,和咱们在第1节中说到的线程同样,multiprocessing.Pool 的代码是创立在 Queue 和 Semaphore 上的,这针对运用其他语言执行多线程和多处理代码的人来讲是很熟练的。
为何多处理版本很重要
这个例子的多处理版本非常好,由于它相对容易设置,并且只必须很少的额外代码。它还充分利用了计算机中的 CPU 资源。在我的设备上,运行它只必须 2.5 秒:
这比咱们看到的其他办法要好得多。
多处理版本的问题
运用多处理有有些缺点。在这个简单的例子中,这些缺点并无透出来,然则将你的问题分解开来,以便每一个处理器都能独立工作有时是很困难的。另外,许多处理方法必须在流程之间进行更加多的通信,这相比非并发程序来讲会繁杂得多。雷锋网
何时运用并发性
首要,你应该判断是不是应该运用并发模块。虽然这儿的示例使每一个库看起来非常简单,但并发性总是伴同着额外的繁杂性,并且常常会导致难以找到的错误。
保持添加并发性,直到显现已知的性能问题,而后确定必须哪种类型的并发性。正如 DonaldKnuth 所说,「过早的优化是编程中所有劫难(或最少大部分劫难)的根源(Premature optimization is the root of all evil (or at least most of it) in programming)」。
一旦你决定优化你的程序,弄清楚你的程序是 CPU 绑定的还是 I/O 绑定的,这便是下一步要做的事情。记住,I/O 绑定的程序是哪些花费大部分时间等待事情完成的程序,而 CPU 绑定的程序则尽可能快地处理数据。
正如你所看到的,CPU 绑定的问题实质上仅有在运用多处理才可处理。线程和异步基本无帮忙处理这类问题。
针对 I/O 绑定的问题,python 社区中有一个通用的经验规则:「能够运用异步,必要运用线程。」异步能够为这种类型的程序供给最佳的速度,但有时必须某些关键库来利用它。记住,任何不放弃对事件循环掌控的任务都将阻塞所有其他任务。
CPU 绑定加速的内容就到此为止啦,认识更加多请拜访原文!
前面的部分请查看:
怎样利用并发性加速你的python程序(一):关联概念
怎样利用并发性加速你的python程序(二):I/O 绑定程序加速
|