为啥以下简单的并行化代码比 Python 中的简单循环慢得多?
Posted
技术标签:
【中文标题】为啥以下简单的并行化代码比 Python 中的简单循环慢得多?【英文标题】:Why is the following simple parallelized code much slower than a simple loop in Python?为什么以下简单的并行化代码比 Python 中的简单循环慢得多? 【发布时间】:2018-03-25 10:09:27 【问题描述】:一个计算数字平方并存储结果的简单程序:
import time
from joblib import Parallel, delayed
import multiprocessing
array1 = [ 0 for i in range(100000) ]
def myfun(i):
return i**2
#### Simple loop ####
start_time = time.time()
for i in range(100000):
array1[i]=i**2
print( "Time for simple loop --- %s seconds ---" % ( time.time()
- start_time
)
)
#### Parallelized loop ####
start_time = time.time()
results = Parallel( n_jobs = -1,
verbose = 0,
backend = "threading"
)(
map( delayed( myfun ),
range( 100000 )
)
)
print( "Time for parallelized method --- %s seconds ---" % ( time.time()
- start_time
)
)
#### Output ####
# >>> ( executing file "Test_vr20.py" )
# Time for simple loop --- 0.015599966049194336 seconds ---
# Time for parallelized method --- 7.763299942016602 seconds ---
这可能是两个选项在数组处理上的区别吗?我的实际程序会有一些更复杂的东西,但这是我需要并行化的那种计算,尽可能简单,但不是这样的结果。
System Model: HP ProBook 640 G2, Windows 7,
IDLE for Python System Type: x64-based PC Processor:
Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz,
2401 MHz,
2 Core(s),
4 Logical Processor(s)
【问题讨论】:
如果您将map(myfun, range(100000))
与并行代码进行比较,这可能是一个公平的比较。哦,顺便说一句,i ^ 2
是i XOR 2
,如果你想平方i
,你需要i ** 2
。
谢谢;更正了^。我如何将非并行简单执行与最快的并行计算进行比较?
python 中的函数调用有很大的开销,只需将没有函数调用的第一个循环与map(myfun, range(100000))
进行比较,没有任何并行性。如果您没有在非并行代码中使用该函数,则函数调用开销会严重影响结果。无论哪种方式,多处理都是一种权衡,在您使用实际程序进行尝试和测量之前,您不会知道它是否是一个好的权衡。
关于函数调用的信息很有用,我正在尝试使用 map()。实际上,我试图首先对此进行排序,因为这似乎是一个容易实现的目标(与我需要的更多涉及的计算相比),但我现在相信它可能不是这样。我认为应该有一些设施可以将每个处理器内核分配给 for 循环的某些部分。说核心 0 表示索引 0 到 3,核心 1 表示 4 到 7 等等。
【参考方案1】:
为什么?因为试图在以下情况下使用工具,工具主要不能也不会调整进入成本:
我喜欢蟒蛇。
我祈祷教育工作者更好地解释工具的成本,否则我们会迷失在这些希望获得的[PARALLEL]
-schedules中。
几个事实:
No.0:通过大量的简化,python 故意使用 GIL 来 [SERIAL]
-ise 访问变量,从而避免[CONCURRENT]
修改的任何潜在冲突 - 在额外的时间支付 GIL 步进舞蹈的这些附加成本
No.1:[PARALLEL]
-代码执行比“公正”-[CONCURRENT]
(read more)更难
No.2:[SERIAL]
-process 必须支付额外费用,如果试图将工作分配给[CONCURRENT]
-workers
No.3:如果一个进程进行工作间通信,每次数据交换都会付出巨大的额外成本
No.4:如果硬件没有足够的资源用于[CONCURRENT]
进程,结果会变得更糟
了解一下标准 python 2.7.13 可以做什么:
效率在于更好地使用芯片,而不是将语法构造器推入合法的区域,但它们的性能会对被测实验的端到端速度产生不利影响:
您支付大约 8 ~ 11 [ms]
只是为了迭代地组装一个空 array1
>>> from zmq import Stopwatch
>>> aClk = Stopwatch()
>>> aClk.start();array1 = [ 0 for i in xrange( 100000 ) ];aClk.stop()
9751L
10146L
10625L
9942L
10346L
9359L
10473L
9171L
8328L
(Stopwatch().stop()
方法从 .start()
生成 [us]
)
而内存效率高、可向量化、无 GIL 的方法也可以做到这一点快 +230 倍 ~ +450 倍:
>>> import numpy as np
>>>
>>> aClk.start();arrayNP = np.zeros( 100000 );aClk.stop()
15L
22L
21L
23L
19L
22L
>>> aClk.start();arrayNP = np.zeros( 100000, dtype = np.int );aClk.stop()
43L
47L
42L
44L
47L
因此,使用适当的工具才刚刚开始性能故事:
>>> def test_SERIAL_python( nLOOPs = 100000 ):
... aClk.start()
... for i in xrange( nLOOPs ): # py3 range() ~ xrange() in py27
... array1[i] = i**2 # your loop-code
... _ = aClk.stop()
... return _
虽然简单的 [SERIAL]
迭代实现是可行的,但您选择这样做会付出巨大的代价~ 70 [ms]
以获得 100000 维向量:
>>> test_SERIAL_python( nLOOPs = 100000 )
70318L
69211L
77825L
70943L
74834L
73079L
使用更合适/合适的工具只需 ~ 0.2 [ms]即++350x 更快
>>> aClk.start();arrayNP[:] = arrayNP[:]**2;aClk.stop()
189L
171L
173L
187L
183L
188L
193L
还有另一个小故障,也就是就地作案手法:
>>> aClk.start();arrayNP[:] *=arrayNP[:];aClk.stop()
138L
139L
136L
137L
136L
136L
137L
产量 ~ +514x 速度,仅通过使用适当的工具
表演的艺术不在于遵循听起来像营销的主张关于并行化-(不惜一切代价),而在于使用知道- 基于方法的方法,以最低的成本实现最大的加速。
对于“小”问题,分发“瘦”工作包的典型成本确实很难被任何可能实现的加速所覆盖,因此“问题大小”实际上限制了人们对方法的选择,这可能会达到积极的效果增益(在 *** 上经常报告 0.9 甚至
结语
处理器数量计数。
核心数量很重要。
但是缓存大小 + NUMA 不规则性比这更重要。
智能、矢量化、HPC 固化、无 GIL 的库很重要 (numpy
等 - 非常感谢 Travis OLIPHANT 等人......向他的团队致敬......)
As an overhead-strict Amdahl Law (re-)-formulation explains, 为什么甚至 many-N
-CPU 并行代码执行可能(而且确实经常这样做)会受到加速
阿姆达尔定律加速的开销严格公式 S
包括付费的 [PAR]-
Setup + [PAR]-
Terminate Overheads 的成本,明确:
1
S = __________________________; where s, ( 1 - s ), N were defined above
( 1 - s ) pSO:= [PAR]-Setup-Overhead add-on
s + pSO + _________ + pTO pTO:= [PAR]-Terminate-Overhead add-on
N
(an interactive animated tool for 2D visualising effects 这些性能限制被引用here)
【讨论】:
非常感谢您提供如此丰富且“有益健康”的答案。我需要一些时间来消化它! 很高兴您被论证所吸引。随意尝试动画交互式滑块,看看实际开销在多长时间内会消除任何数量的 N-CPU-s 带来的积极影响。 这是关于性能的故事中最重要的部分。 粗略地说,据我了解,添加 n 个内核只会增加 n 倍(对于大多数用户来说,n 通常 让我按照逻辑 - 拥有 N-worker 可能有助于拆分,但只有整个流程任务的 [PARALLEL]-fraction S,让 [SERIAL] 部分持续相同的时间,因为它主要是 [SERIAL] 并且任何数量的 N-worker 只需等待,直到 [SERIAL] 部分完成。另外,您总是需要支付成本——这里是所有初始和终端开销的集合——无论是与处理相关的、与内存管理相关的、与工作包分配相关的还是其他任何需要及时支付的自然部分运行 [PARALLEL]-fraction。 所以,如果[PARALLEL]
-fraction 在N
-workers 上的理论上的理想分割使得[PARALLEL]
-部分过程持续时间从 5 秒缩短到 1 秒(这 5:1 理论上 看起来是一个勇敢的步骤)但是如果您必须支付(为了实现这种类型的处理安排)+4 秒的成本,那么您在这里不会得到任何加速。同样,如果您碰巧加速(使用更智能的工具、更高效的数据布局等),[SERIAL]
-part 不会持续 5 秒,而只会持续 4 秒(没有任何额外开销) ,你有 10% 的净加速。【参考方案2】:
来自threading
的documentation:
如果你知道你正在调用的函数是基于编译的 释放 Python 全局解释器锁 (GIL) 的扩展 在其大部分计算期间...
问题在于,在这种情况下,您不知道这一点。 Python 本身只允许一个线程同时运行(python 解释器每次执行 python 操作时都会锁定 GIL)。
threading
仅在myfun()
将大部分时间花在编译的 Python 扩展中时才有用,并且该扩展释放了 GIL。
Parallel
代码慢得令人尴尬,因为您正在做大量工作来创建多个线程 - 然后您一次只能执行一个线程。
如果您使用multiprocessing
后端,则必须将输入数据复制到四个或八个进程中的每一个(每个内核一个),在每个进程中进行处理,然后将输出数据复制回来。复制会很慢,但如果处理比计算平方复杂一点,那可能是值得的。测量并查看。
【讨论】:
感谢您的回答。这是否意味着为了加快这样的计算,我需要在命令行中做一些事情?或者我仍然可以在我的代码中加速这样的计算吗? 这意味着要加速这样的计算你需要使用'not python'......线程不会加速像python中那样的事情,并且多处理非常昂贵并且通信并不那么容易与线程一样,因此您要么咬紧牙关运行单线程,要么找到替代语言/python 实现。 你没有解释你的用例。你真的想为了计算一个元素的平方根而发起威胁吗? @Ramast:不,当然不是。这个问题实际上是说“我的实际程序会有更复杂的东西”,但作为一个优秀的问题作者,他已经剪掉了不相关的细节并用简单的东西代替了它。 @Ramast 预先计算,应该在fatFUN()
“内部”发生的最小处理量是相当公平的>,至少“调整/覆盖”两个[PAR]-
Processing-Setup-| [PAR]-
Processing-Terminate--开销。有了这个定量支持的系数,您的主张可能会得到更好的支持,即从“if myfun()
开始很多 处理”,不是吗? 这是任何严肃的 HPC 设计实践都必须开始的地方。以上是关于为啥以下简单的并行化代码比 Python 中的简单循环慢得多?的主要内容,如果未能解决你的问题,请参考以下文章
为啥在更多 CPU/内核上的并行化在 Python 中的扩展性如此之差?
在这种情况下,为啥串行代码比 concurrent.futures 快?
python dask DataFrame,支持(可简单并行化)行吗?