为啥以下简单的并行化代码比 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 ^ 2i 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,支持(可简单并行化)行吗?

如何并行化具有多个参数的简单 python def [重复]

并行代码比串行代码慢(值函数迭代示例)

超简单的Python教程系列——第16篇:多进程