我有一组矢量并计算他们的差异与第一个的差异。 使用python广播时,计算速度明显慢于通过简单循环进行计算。为什么呢?
import numpy as np
def norm_loop(M, v):
n = M.shape[0]
d = np.zeros(n)
for i in range(n):
d[i] = np.sum((M[i] - v)**2)
return d
def norm_bcast(M, v):
n = M.shape[0]
d = np.zeros(n)
d = np.sum((M - v)**2, axis=1)
return d
M = np.random.random_sample((1000, 10000))
v = M[0]
%timeit norm_loop(M,v) - > 25.9毫秒
%timeit norm_bcast(M,v) - > 38.5毫秒
我有Python 3.6.3和Numpy 1.14.2
在google colab中运行示例: https://drive.google.com/file/d/1GKzpLGSqz9eScHYFAuT8wJt4UIZ3ZTru/view?usp=sharing
答案 0 :(得分:6)
内存访问。
首先,广播版可以简化为
def norm_bcast(M, v):
return np.sum((M - v)**2, axis=1)
这仍然比循环版本稍慢。 现在,传统观点认为使用广播的矢量化代码应该总是更快,这在许多情况下是不正确的(我将无耻地插入我的另一个答案here)。那么发生了什么?
正如我所说,它归结为内存访问。
在广播版本中,从v中减去M的每个元素。到处理M的最后一行时,处理第一行的结果已从缓存中逐出,因此对于第二步,这些差异再次加载到缓存和平方。最后,它们被加载并第三次处理以进行求和。由于M非常大,因此每一步都会清除部分缓存,以便记录所有数据。
在循环版本中,每一行都在一个较小的步骤中完全处理,从而减少了缓存未命中次数和总体更快的代码。
最后,使用einsum
可以通过一些数组操作来避免这种情况。
此功能允许混合矩阵乘法和求和。
首先,我会指出它是一个与其他numpy相比具有相当不直观的语法的函数,并且潜在的改进通常不值得花费额外的努力去理解它。
由于舍入误差,答案也可能略有不同。
在这种情况下,它可以写成
def norm_einsum(M, v):
tmp = M-v
return np.einsum('ij,ij->i', tmp, tmp)
这会将它减少到整个数组上的两个操作 - 减法,并调用einsum
,执行平方和求和。
这略有改进:
%timeit norm_bcast(M, v)
30.1 ms ± 116 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit norm_loop(M, v)
25.1 ms ± 37.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit norm_einsum(M, v)
21.7 ms ± 65.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
答案 1 :(得分:1)
在矢量化操作上,您显然存在错误的缓存行为。但由于没有利用现代SIMD指令(AVX2,FMA),它的计算速度也很慢。幸运的是,克服这些问题并不是很复杂。
示例强>
import numpy as np
import numba as nb
@nb.njit(fastmath=True,parallel=True)
def norm_loop_improved(M, v):
n = M.shape[0]
d = np.empty(n,dtype=M.dtype)
#enables SIMD-vectorization
#if the arrays are not aligned
M=np.ascontiguousarray(M)
v=np.ascontiguousarray(v)
for i in nb.prange(n):
dT=0.
for j in range(v.shape[0]):
dT+=(M[i,j]-v[j])*(M[i,j]-v[j])
d[i]=dT
return d
<强>性能强>
M = np.random.random_sample((1000, 1000))
norm_loop_improved: 0.11 ms**, 0.28ms
norm_loop: 6.56 ms
norm_einsum: 3.84 ms
M = np.random.random_sample((10000, 10000))
norm_loop_improved:34 ms
norm_loop: 223 ms
norm_einsum: 379 ms
** 衡量效果时要小心
第一个结果(0.11ms)来自使用相同数据反复调用该函数。这需要77 GB / s的RAM读取数量,这远远超过我的DDR3 Dualchannel-RAM能力。由于连续调用具有相同输入参数的函数并不现实,我们必须修改测量。
为了避免这个问题,我们必须使用不同的数据至少两次调用相同的函数(8MB L3缓存,8MB数据),然后将结果除以2以清除所有缓存。
此方法的相对性能在数组大小上也有所不同(请查看einsum结果)。