Python Numpy就地运行性能

Python Numpy就地运行性能,python,numpy,Python,Numpy,我在比较numpy阵列就地操作和常规操作。 下面是我所做的(Python版本3.7.3): a1,a2=np.random.random((10,10)),np.random.random((10,10)) 为了进行比较: def func1(a1、a2): a1=a1+a2 def func2(a1、a2): a1+=a2 %timeit func1(a1,a2) %时间函数2(a1,a2) 因为就地操作避免了为每个循环分配内存。我原以为func1比func2慢 但是我得到了这个: [10

我在比较numpy阵列就地操作和常规操作。 下面是我所做的(Python版本3.7.3):

a1,a2=np.random.random((10,10)),np.random.random((10,10))
为了进行比较:

def func1(a1、a2):
a1=a1+a2
def func2(a1、a2):
a1+=a2
%timeit func1(a1,a2)
%时间函数2(a1,a2)
因为就地操作避免了为每个循环分配内存。我原以为
func1
func2

但是我得到了这个:

[10]中的
:%timeit func1(a1,a2)
每个回路595纳秒±14.4纳秒(7次运行的平均值±标准偏差,每个1000000个回路)
在[11]:%timeit func2(a1,a2)
每个回路1.38µs±7.87 ns(7次运行的平均值±标准偏差,每个1000000个回路)
在[12]中:np.\u版本__
Out[12]:“1.16.2”
这表明
func1
只占
func2
所用时间的1/2。
有人能解释一下为什么会出现这种情况吗?

因为您忽略了对小矩阵进行矢量化操作和预取的影响

请注意,矩阵的大小(10 x 10)很小,因此分配临时存储所需的时间并不太长(但),对于具有较大缓存大小的处理器,这些小矩阵可能仍然完全适合一级缓存,因此,对这些小矩阵执行矢量化操作等所获得的速度增益将超过分配临时矩阵所损失的时间以及直接添加到一个分配的内存位置所获得的速度增益

但是当你增加矩阵的大小时,情况就不同了

In [41]: k = 100

In [42]: a1, a2 = np.random.random((k, k)), np.random.random((k, k))

In [43]: %timeit func2(a1, a2)
4.41 µs ± 3.01 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [44]: %timeit func1(a1, a2)
6.36 µs ± 4.18 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [45]: k = 1000

In [46]: a1, a2 = np.random.random((k, k)), np.random.random((k, k))

In [47]: %timeit func2(a1, a2)
1.13 ms ± 1.49 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [48]: %timeit func1(a1, a2)
1.59 ms ± 2.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [49]: k = 5000
In [50]: a1, a2 = np.random.random((k, k)), np.random.random((k, k))

In [51]: %timeit func2(a1, a2)
30.3 ms ± 122 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [52]: %timeit func1(a1, a2)
94.4 ms ± 58.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
编辑:这是为了
k=10
显示您在我的机器上观察到的小矩阵也是正确的

In [56]: k = 10

In [57]: a1, a2 = np.random.random((k, k)), np.random.random((k, k))

In [58]: %timeit func2(a1, a2)
1.06 µs ± 10.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [59]: %timeit func1(a1, a2)
500 ns ± 0.149 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

我觉得这很有趣,于是决定自己来计时。但是,我没有只检查10x10阵列,而是使用NumPy 1.16.2测试了许多不同的阵列大小:

这清楚地表明,对于较小的阵列尺寸,常规加法更快,只有对于较大的阵列尺寸,就地操作更快。在100000个元素周围还有一个奇怪的突起,我无法解释(它接近我电脑上的页面大小,可能使用了不同的分配方案)

分配临时阵列的速度预计会较慢,因为:

  • 我们必须分配内存
  • 必须迭代3个数组才能执行操作,而不是2个
特别是第一个点(分配内存)可能没有在基准中说明(不使用
%timeit
不使用
简单的\u基准。运行
)。这是因为反复请求相同的内存大小可能是非常优化的。这将使添加额外数组的速度看起来比实际速度快一点

这里要提到的另一点是,就地加法可能有一个更高的常数因子。如果要进行就地添加,则必须先进行更多代码检查,然后才能执行操作,例如重叠输入。这可能会在适当的地方增加一个更高的常数因子

更一般的建议是:微观基准可能会有所帮助,但它们并不总是真正准确的。您还应该对调用它的代码进行基准测试,以便对代码的实际性能做出更有教育意义的陈述。通常,这样的微基准测试会遇到一些高度优化的情况(例如反复分配相同数量的内存并再次释放),而这在实际使用代码时是不会发生的(经常发生)

以下是我使用我的库用于图形的代码:

从simple_benchmark导入BenchmarkBuilder,多参数
将numpy作为np导入
b=基准构建器()
@b、 添加函数()
def功能1(a1、a2):
a1=a1+a2
@b、 添加函数()
def func2(a1、a2):
a1+=a2
@b、 添加参数('数组大小')
def参数_提供程序():
对于范围(3,28)内的exp:
尺寸=整数(1.4**exp)
a1=np.random.random([尺寸大小,尺寸大小])
a2=np.random.random([尺寸大小,尺寸大小])
产量尺寸**2,多参数([a1,a2])
r=b.运行()
r、 绘图()

我觉得这两个操作都是矢量化的。就地操作没有矢量化有什么原因吗?我没有说就地操作没有矢量化。事实上,
func1
func2
中的操作很可能是由编译器以不同的方式进行矢量化的(如果有的话)。我的意思是,人们不应该低估这些向量化操作对完全可以放入顶级缓存的数据(包括临时存储)的性能。性能降低通常发生在每个级别的缓存大小限制处,这会导致缓存未命中以及需要在缓存之间移动数据,这是矩阵大小变大时我们会遇到的情况。谢谢,这是一个很好的解释。所以基本上你是说额外的分配不是问题。我仍然不清楚为什么一个会比另一个好(很多!)。你的答案似乎表明时间应该是可比较的。时间是不可比较的,因为具体的矢量化操作可能非常不同。这必须与预取和缓存未命中的影响一起考虑。此外,数组大小的差异还可能导致运行不同的代码(除非检查Numpy的代码,否则我无法确定)。我的观点是,不能仅仅根据我们天真地认为所涉及的操作,就指望
func1
func2
执行得慢/快。例如,当Numpy使用多线程BLAS(如OpenBLAS等)时,事情会变得更加复杂。再加上,这种现象在科学计算领域是众所周知的。你可以查阅相关的科学计算/数值算法/分布