哇!绝对喜欢你的代码。它就像一种魅力,将总效率提高了 400 倍。我将尝试阅读有关 numba 和 jit 编译器的更多信息,但您能否简要说明一下为什么它如此高效。再次感谢所有的帮助!–拉姆 2018年1 月 3 日 20:30
我们可以很容易地到达下面77 [ms]
的某个地方,但是需要掌握几个步骤才能到达那里,所以让我们开始吧:
问:为什么并行操作需要更多时间?
因为提议的步骤joblib
创建了那么多完整的流程副本——以便摆脱 GIL步骤的纯[SERIAL]
跳舞(一个接一个)但是(!)这包括所有内存传输的附加成本(非常所有变量和整个python解释器及其内部状态的昂贵/敏感numpy
),在它开始对你的“有效载荷”计算策略的“有用”工作进行第一步之前,
所以
所有的总和这些实例化开销很容易变得比与开销无关的反比例1 / N
因子预期更大,
您在其中设置N ~ num_cores
.
有关详细信息,请在此处阅读阿姆达尔定律重新公式化尾部的数学公式。
Q:可以帮助提高上述代码的效率吗?
尽可能多地节省所有间接成本:
- 在可能的情况下:
-在进程生成端,尝试使用n_jobs = ( num_cores - 1 )
为“主”进程留出更多空间,并在性能上升时进行基准测试
-在进程终止端,避免从返回值中收集并构造一个新的(可能很大)对象,而是预先分配一个足够大的本地进程数据结构并返回一些有效的、序列化的,以便于每个对象的简单和非阻塞合并-partes 返回结果的对齐方式。
这两个“隐藏”成本都是您的主要设计敌人,因为它们会线性添加到[SERIAL]
整个问题解决方案的计算路径的纯部分(参考:这两者在开销严格的阿姆达尔定律中的影响公式)
实验与结果:
>>> from zmq import Stopwatch; aClk = Stopwatch()
>>> base_array = np.ones( (2**12, 2**12), dtype = np.uint8 )
>>> base_array.flags
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
>>> def compute_average_per_TILE( TILE_i, TILE_j ): // NAIVE MODE
... return np.uint8( np.mean( base_array[ 4*TILE_i:4*(TILE_i+1),
... 4*TILE_j:4*(TILE_j+1)
... ]
... )
... )
...
>>> aClk.start(); _ = compute_average_per_TILE( 12,13 ); aClk.stop()
25110
102
109
93
这大约需要93 [us]
一次拍摄。预计 1024*1024*93 ~ 97,517,568 [us]
将覆盖整个平均处理base_array
。
实验上,这里可以很好地看到管理费用处理得不好的影响,天真的嵌套实验采取了:
>>> aClk.start(); _ = [ compute_average_per_TILE( i, j )
for i in xrange(1024)
for j in xrange(1024)
]; aClk.stop()
26310594
^^......
26310594 / 1024. / 1024. == 25.09 [us/cell]
这大约减少了 3.7倍(由于没有发生“尾部”部分(分配单个返回值))开销 2**20 次,但只有一次,在终端分配。
然而,更多的惊喜还在后面。
什么是合适的工具?
从来没有一个通用的规则,没有一刀切的。
假设
每个调用不超过一个 4x4 矩阵图块(实际上比25 [us]
提议的joblib
-orchestrated2**20
调用的生成要少,分布在.cpu_count()
原始提议的〜完全实例化的进程中)
...( joblib.Parallel( n_jobs = num_cores )(
joblib.delayed( compute_average )( i, j )
for i in xrange( 1024 )
for j in xrange( 1024 )
)
确实有提升性能的空间。
对于这些小规模矩阵(从这个意义上说,并非所有问题都如此令人满意),人们可以期望从更智能的内存访问模式和减少源自 python GIL 的弱点中获得最佳结果。
由于每次调用的跨度只是 4x4 微型计算,因此更好的方法是利用智能矢量化(所有数据都适合缓存,因此缓存内计算是追求最佳性能的假期之旅)
最好的(仍然非常幼稚的矢量化代码)
能够从小于~ 25 [us/cell]
(仍然有空间进行更好的对齐处理,因为它需要/一个单元处理),所以如果缓存内优化,可以期待另一个级别的加速矢量化代码将被正确制作。~ 74 [ns/cell]
~ 4.6 [ns]
base_array
在77 [ms]
?!值得这样做,不是吗?
不是 97 秒,
也不是 25 秒,
但比77 [ms]
敲几下键盘还短,如果更好地优化呼号,还可以挤出更多时间:
>>> import numba
>>> @numba.jit( nogil = True, nopython = True )
... def jit_avg2( base_IN, ret_OUT ): // all pre-allocated memory for these data-structures
... for i in np.arange( 1024 ): // vectorised-code ready numpy iterator
... for j in np.arange( 1024 ):// vectorised-code ready numpy iterator
... ret_OUT[i,j] = np.uint8( np.mean( base_IN[4*i:4*(i+1),
... 4*j:4*(j+1)
... ]
... )
... )
... return // avoid terminal assignment costs
...
>>> aClk.start(); _ = jit_avg2( base_array, mean_array ); aClk.stop()
1586182 (even with all the jit-compilation circus, it was FASTER than GIL-stepped nested fors ...)
76935
77337