目录
并行性的困扰:当更快的代码变得更慢
当你使用NumPy进行计算密集型操作时,你可能会希望使用计算机的所有CPU。你的计算机有2个、4个甚至更多的CPU核心,如果你能充分利用它们,你的代码会运行得更快。
当然,除了并行性有时会让你的代码变得更慢。
事实证明,对于某些操作,NumPy会透明地进行并行化处理。如果你不小心,这实际上可能会减慢你的代码。
并行性让代码更快
考虑以下程序:
import numpy as np
a = np.ones((4096, 4096))
a.dot(a)
如果我们使用time
工具运行这段代码,结果如下:
$ time python dot.py
real 0m1.546s
user 0m4.171s
sys 0m0.537s
real
时间是挂钟时间,user
时间是CPU时间。在这个例子中,user
时间比real
时间高,这意味着操作使用了多个CPU,总共消耗了4.17秒的CPU时间。
这很棒!如果我们只使用一个CPU,这个操作将花费约4.2秒,但由于使用了多个CPU,它只花费了约1.5秒。
并行性让代码更慢
让我们验证这个假设。
我们将通过使用threadpoolctl库告诉NumPy只使用一个CPU:
import numpy as np
from threadpoolctl import threadpool_limits
with threadpool_limits(limits=1, user_api='blas'):
a = np.ones((4096, 4096))
a.dot(a)
现在当我们运行它时:
$ time python dot_onecpu.py
real 0m3.654s
user 0m3.652s
sys 0m0.403s
当我们使用多个CPU时,它花费了约4.2秒的CPU时间,但使用单个CPU时,它只花费了约3.7秒的CPU时间。从CPU时间的角度来看,代码现在更快了!
这重要吗?难道不是更短的挂钟时间更重要吗?
如果你只在计算机上运行这一个程序,并且你没有为你的程序实现任何其他形式的并行性,那么是的,这没问题。但如果你自己实现了某种形式的并行性,例如使用multiprocessing、joblib,或者我个人最喜欢的Dask,默认的并行性会使你的程序整体变慢。
在这个例子中,每次dot()
调用将占用你整体CPU容量的13%。
BLAS!
你会注意到上面的代码中,线程池限制提到了BLAS。BLAS是一个用于线性代数的API,NumPy在某些操作中使用它,比如这里的dot()
。
有不同的BLAS实现可用,在上面的例子中,我使用的是OpenBLAS。另一个选择是mkl
,它由Intel提供,因此针对Intel处理器进行了优化;你不希望在AMD上使用它。
至少对于这个操作,mkl
似乎也有同样的问题:它在单个CPU上运行得比在多个CPU上并行化时更快。一般来说,值得看看切换是否会给你的性能带来一些提升。
如果你使用的是Conda Forge,你可以通过在environment.yml
中包含blas=*=openblas
或blas=*=mkl
包来实现这一点。
如果这只是这个基准测试呢?
有人可能会说这只是一个基准测试,我可能以多种方式搞砸了。虽然这是事实,但这只是一个例子:
- OpenBLAS的问题跟踪器列出了许多多线程减慢速度的情况;在某些情况下,禁用多线程是解决方案。
- 从更广泛的角度来看,切换到多线程总是有成本和开销的(另见Amdahl定律)。
因此,如果使用N个线程实际上能带来×N的性能,那将是非常令人惊讶的。
所以,是的,你通常确实需要并行性,但你需要思考在哪里、如何以及何时使用它。
减少并行性以获得更多并行性
如果你使用Dask等工具实现了高层次的并行性,你可能希望禁用NumPy中的多线程。 单个操作将占用更少的CPU时间,而你自己实现的并行性将确保多个CPU的利用率。
此外,在性能分析时要小心:如果你使用自动并行化,你分析的内容可能与具有不同CPU数量的计算机上的行为不匹配。