为什么下面示例中的python广播比简单循环慢?

为什么下面示例中的python广播比简单循环慢?,python,numpy,Python,Numpy,我有一个向量数组,计算它们的微分与第一个向量的范数。 使用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 =

我有一个向量数组,计算它们的微分与第一个向量的范数。 使用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 ms

%timeit norm_bcast(M, v)
38.5 ms
我有Python 3.6.3和Numpy 1.14.2

要在google colab中运行该示例,请执行以下操作: 内存访问

首先,广播版本可以简化为

def norm_bcast(M, v):
     return np.sum((M - v)**2, axis=1)
这仍然比循环版本运行稍慢。 现在,传统观点认为使用广播的矢量化代码应该总是更快,但在很多情况下这是不对的(我会无耻地插入我的另一个答案)。发生了什么事

正如我所说,它归结为内存访问

在广播版本中,M的每个元素都从v中减去。在处理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)
内存访问

首先,广播版本可以简化为

def norm_bcast(M, v):
     return np.sum((M - v)**2, axis=1)
这仍然比循环版本运行稍慢。 现在,传统观点认为使用广播的矢量化代码应该总是更快,但在很多情况下这是不对的(我会无耻地插入我的另一个答案)。发生了什么事

正如我所说,它归结为内存访问

在广播版本中,M的每个元素都从v中减去。在处理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)
挤出最大性能 在矢量化操作中,显然存在不好的缓存行为。但由于没有利用现代SIMD指令(AVX2、FMA),计算itsef也很慢。幸运的是,克服这些问题并不复杂

示例

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
性能

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)来自使用相同数据重复调用函数。这将需要从RAM读取77 GB/s的数据,这远远超过我的DDR3双通道RAM的能力。由于连续调用具有相同输入参数的函数根本不现实,我们必须修改度量

为了避免这个问题,我们必须用不同的数据调用同一个函数至少两次(8MB L3缓存,8MB数据),然后将结果除以2以清除所有缓存

这种方法的相对性能在阵列大小上也有所不同(请查看einsum结果)。

挤出最大性能 在矢量化操作中,显然存在不好的缓存行为。但由于没有利用现代SIMD指令(AVX2、FMA),计算itsef也很慢。幸运的是,克服这些问题并不复杂

示例

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
性能

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)来自使用相同数据重复调用函数。这将需要从RAM读取77 GB/s的数据,这远远超过我的DDR3双通道RAM的能力。由于连续调用具有相同输入参数的函数根本不现实,我们必须修改度量

为了避免这个问题,我们必须用不同的数据调用同一个函数至少两次(8MB L3缓存,8MB数据),然后将结果除以2以清除所有缓存


这种方法的相对性能在数组大小上也有所不同(请查看einsum结果)。

如果在norm_循环中更改为
d[i]=np.linalg.norm(M[i]-v)
,则可以节省更多的时间。奇怪的是,我用einsum得到的结果较慢。可能是由于OpenBLAS。如果在norm\u循环中更改为
d[i]=np.linalg.norm(M[i]-v)
,则可以节省更多的时间。s