脾气暴躁的现场表现

时间:2019-07-14 05:17:00

标签: 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)
%timeit func2(a1, a2)

因为就地操作避免了为每个循环分配内存。我原以为func1会比func2慢。

但是我得到了:

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

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

In [12]: np.__version__
Out[12]: '1.16.2'

这暗示func1仅是func2所花费时间的1/2。 谁能帮助解释为什么会这样?

2 个答案:

答案 0 :(得分:4)

因为您忽略了向量化运算和小矩阵预取的影响。

请注意,矩阵的大小(10 x 10)很小,因此分配临时存储所需的时间还不那么大(尚未),对于高速缓存大小较大的处理器,这些较小的矩阵可能仍适合L1完全缓存,因此对这些小型矩阵执行矢量化操作等获得的速度增益将远远超过分配临时矩阵所浪费的时间,以及直接添加到分配的存储位置之一中所获得的速度增益。

但是当您增加矩阵的大小时,故事就变得不同

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)

答案 1 :(得分:2)

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

enter image description here

这清楚地表明,对于较小的阵列大小,正常添加速度更快,并且仅对于中等大小的阵列,就地操作速度更快。在我无法解释的100000个元素周围还有一个怪异的变化(它接近计算机上的页面大小,也许使用了不同的分配方案)。

预计分配临时数组 会比较慢,原因是:

  • 必须分配该内存
  • 一个必须迭代3个数组才能执行操作,而不是2个。

特别是在基准测试中可能没有考虑到第一点(分配内存)(不是用%timeit而不是simple_benchmark.run)。那是因为一遍又一遍地请求相同的内存大小可能是非常优化的。这样会使带有额外数组的加法看起来比实际速度快一点。

这里要提到的另一点是,就地加法可能具有更高的常数因子。如果要进行就地添加,则必须先进行更多代码检查,然后才能执行操作,例如,重叠输入。这样就可以赋予原位加法更高的恒定因子。

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

这也是我使用库simple_benchmark用于图形的代码:

from simple_benchmark import BenchmarkBuilder, MultiArgument
import numpy as np

b = BenchmarkBuilder()

@b.add_function()
def func1(a1, a2):
    a1 = a1 + a2

@b.add_function()
def func2(a1, a2):
    a1 += a2

@b.add_arguments('array size')
def argument_provider():
    for exp in range(3, 28):
        dim_size = int(1.4**exp)
        a1 = np.random.random([dim_size, dim_size])
        a2 = np.random.random([dim_size, dim_size])
        yield dim_size ** 2, MultiArgument([a1, a2])

r = b.run()
r.plot()