Wow! Absolutely loved your code. It worked like a charm improving the total efficiency by 400x. I'll try to read more about numba and jit compilers, but can you write briefly of why it is so efficient. Thanks once again for all the help! – Ram https://stackoverflow.com/users/4522548/ram Jan 3 '18 at 20:30
We can quite easily get somewhere under 77 [ms]
, but it takes mastering a few steps to get there, so let's start:
问:为什么并行操作需要更多时间?
因为建议的步骤是joblib
创建许多完整的进程副本 - 以便摆脱 GIL 步进pure-[SERIAL] https://stackoverflow.com/revisions/8337936/5跳舞(一个接一个)但是(!)这包括所有内存传输的附加成本(对于确实很大的内存传输非常昂贵/敏感)numpy
数组)的所有变量和整个Python解释器及其内部状态,在它开始对你的“有效负载”计算策略进行“有用”工作的第一步之前,
so
所有这些实例化开销的总和很容易变得比与开销无关的反比例期望更大1 / N
factor,
你设置的地方N ~ num_cores
.
For details,阅读阿姆达尔定律重新公式化尾部的数学公式here. https://stackoverflow.com/revisions/18374629/3
Q:上面的代码可以帮助提高效率吗?
尽可能节省所有管理费用:
- 在可能的情况:
- 在进程生成端,尝试使用n_jobs = ( num_cores - 1 )
为“主要”流程的推进和性能提升时的基准测试留出更多空间
- 在进程终止端,避免从返回值中收集和构造一个新的(可能尺寸很大)对象,而是预先分配一个足够大的进程本地数据结构,并返回一些高效的序列化对象,以便轻松且无阻塞地合并每个部分返回结果的对齐方式。
这两种“隐藏”成本都是您的主要设计敌人,因为它们会线性添加到纯成本中。[SERIAL]
整个问题解决方案的计算路径的一部分(ref.:这两者的影响在严格开销阿姆达尔定律公式 https://stackoverflow.com/revisions/18374629/3 )
实验和结果:
>>> 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 次,但仅在终端分配时一次。
然而,更多惊喜还在后面。
这里什么是合适的工具?
从来没有一个放之四海而皆准的规则,也没有放之四海而皆准的规则。
Given
每次调用的进程不会超过 4x4 矩阵图块(实际上少于25 [us]
根据提议的joblib
- 精心策划的衍生2**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]
to 少于~ 74 [ns/cell]
(仍然有空间进行更好的对齐处理,因为它需要~ 4.6 [ns]
/ a base_array
细胞处理),因此,如果缓存内优化的矢量化代码能够正确制作,那么预计会得到另一个级别的加速。
In 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