为什么以下简单的并行化代码比Python中的简单循环慢得多?

DS *_*S R 5 python arrays parallel-processing function

一个简单的程序,用于计算数字平方并存储结果:

    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
                                                                 )
            )
Run Code Online (Sandbox Code Playgroud)
    #### Output ####
    # >>> ( executing file "Test_vr20.py" )
    # Time for simple loop         --- 0.015599966049194336 seconds ---
    # Time for parallelized method --- 7.763299942016602 seconds ---
Run Code Online (Sandbox Code Playgroud)

这两个选项的数组处理是否可能有所不同?我的实际程序会有一些更复杂的东西,但这是我需要尽可能并行化的一种计算,但不会得到这样的结果。

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)
Run Code Online (Sandbox Code Playgroud)

use*_*197 5

为什么?因为在工具原则上不能也不会调整进入成本的
情况下尝试使用工具:

我喜欢蟒蛇。
我祈祷教育工作者更好地解释工具的成本,否则我们会迷失在这些希望得到的[PARALLEL]时间表中。

一些事实:

No. 0:通过大量简化,Python 有意使用GIL[SERIAL]访问变量,从而避免[CONCURRENT]修改造成的任何潜在冲突 - 在额外的时间内支付 GIL-stepped dance 的附加成本
No. 1[PARALLEL]-代码执行比“只是”要困难得多- [CONCURRENT]阅读更多
第二:如果试图将工作分配给工人,则进程必须支付额外成本第三:如果一个进程进行工人间通信,则每个进程必须支付巨大的额外成本第四:如果硬件用于处理的资源很少,结果会变得更糟[SERIAL][CONCURRENT][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
Run Code Online (Sandbox Code Playgroud)

(该Stopwatch().stop()方法[us]从得出.start()
,而内存高效、可向量化、无 GIL 的方法可以更快地完成相同的操作,速度大约 +230x ~ +450x:

>>> 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
Run Code Online (Sandbox Code Playgroud)

因此,使用正确的工具只是性能故事的开始:

>>> 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 _
Run Code Online (Sandbox Code Playgroud)

虽然简单的迭代实现有效,但选择对 100000 维向量[SERIAL]执行此操作会付出巨大的成本:~ 70 [ms]

>>> test_SERIAL_python( nLOOPs = 100000 )
 70318L
 69211L
 77825L
 70943L
 74834L
 73079L
Run Code Online (Sandbox Code Playgroud)

使用更合适/合适的工具仅需约 0.2 [ms]
,即 ++350 倍更快

>>> aClk.start();arrayNP[:] = arrayNP[:]**2;aClk.stop()
189L
171L
173L
187L
183L
188L
193L
Run Code Online (Sandbox Code Playgroud)

还有另一个小故障,又名就地操作方式:

>>> aClk.start();arrayNP[:] *=arrayNP[:];aClk.stop()
138L
139L
136L
137L
136L
136L
137L
Run Code Online (Sandbox Code Playgroud)

只需使用适当的工具,即可获得约 +514 倍的速度

性能的艺术不在于遵循听起来像营销的
并行化主张不惜任何代价
而在于使用基于专有技术的方法,以最少的成本获得最大的加速。

对于“小”问题,分发“薄”工作包的典型成本确实很难通过任何可能实现的加速来覆盖,因此“问题大小”实际上限制了人们对方法的选择,而这可以实现正收益(加速StackOverflow 上经常报道 0.9 甚至 << 1.0,因此您不必在这种惊喜中感到迷失或孤独)。


结语

处理器数量很重要。
核心数量很重要。
但缓存大小 + NUMA 不规则性的影响远不止于此。
智能、矢量化、HPC 固化、无 GIL 库很重要
numpy等人 - 非常感谢 Travis OLIPHANT 等人......向他的团队致敬......)


正如开销严格的阿姆达尔定律(重新)公式所解释的那样,为什么即使是许多NCPU并行代码执行也可能(而且确实经常)受到加速 << 1.0 的影响

阿姆达尔定律加速的开销严格制定包括支付的设置 +终止开销S的成本,明确[PAR]-[PAR]-

               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               
Run Code Online (Sandbox Code Playgroud)

这里引用了一个交互式动画工具,用于对这些性能限制进行 2D 可视化效果


Mar*_*ica 4

来自以下文档threading

如果您知道您正在调用的函数基于已编译的扩展,该扩展会在其大部分计算期间释放 Python 全局解释器锁 (GIL)...

问题是在这种情况下,你不知道这一点。Python 本身只允许一个线程同时运行(Python 解释器每次执行 Python 操作时都会锁定 GIL)。

threadingmyfun()仅当大部分时间都花在已编译的 Python 扩展上并且该扩展释放了 GIL时才会有用。

Parallel代码速度慢得令人尴尬,因为您需要做大量工作来创建多个线程,然后您一次只能执行一个线程。

如果使用multiprocessing后端,则必须将输入数据复制到四个或八个进程中的每一个(每个核心一个),在每个进程中进行处理,然后将输出数据复制回来。复制会很慢,但如果处理比仅仅计算平方更复杂一点,那么可能是值得的。测量并查看。

  • 这意味着要加速这样的计算,你需要使用“不是Python”......线程不会加速Python中的速度,并且多处理非常昂贵,并且通信不像线程那么容易,所以你要么咬子弹并运行单线程或找到替代语言/python 实现。 (2认同)
  • @拉马斯特:不,当然不是。这个问题实际上是说“我的实际程序会有更复杂的东西”,但作为一个优秀的问题作者,他已经剪掉了不相关的细节并用简单的东西代替了它。 (2认同)