为什么numpy.take在numpy.split的结果上变慢,而简单的索引不是?

时间:2018-04-17 13:50:15

标签: python performance numpy multidimensional-array

考虑以下代码

import timeit
import numpy as np

MyArray = np.empty((10000, 10000, 1))
print((MyArray.size, MyArray.shape, MyArray.dtype, np.isfortran(MyArray)))
print(timeit.timeit(lambda: MyArray[0], number=10000))
print(timeit.timeit(lambda: MyArray.take(0), number=10000))

MyTwoArrays = np.empty((10000, 10000, 2))
MyArray = np.split(MyTwoArrays, 2, axis=2)[0]
print((MyArray.size, MyArray.shape, MyArray.dtype, np.isfortran(MyArray)))
print(timeit.timeit(lambda: MyArray[0], number=10000))
print(timeit.timeit(lambda: MyArray.take(0), number=1))

及其在我系统上的输出:

(100000000, (10000, 10000, 1), dtype('float64'), False)
0.05690493136299911
0.06236779451013045
(100000000, (10000, 10000, 1), dtype('float64'), False)
0.0569617025453055
1.6303121549025763

MyArray的两个版本具有相同的大小,形状,数据类型和数据排序。尽管如此,与使用numpy.take结果的numpy.split相比,获得第0个元素的速度要慢300,000倍,而使用相同结果的简单索引或使用“本机”numpy数组的numpy.take。 / p>

为什么?我可以解决这个问题吗?

更新

这似乎与视图有关:MyArray = MyArray.copy()修复了问题。尽管如此,我仍然感兴趣的是[0]为什么同样快速地工作,而numpy.take对视图的速度变慢。

另一次更新:

我注意到减速取决于数组维度(不是数组维度的数量,而是数组元素的数量)。对于单个第0个元素,我实现了长达8秒的访问时间。我发现这是这个问题最令人惊讶的方面。无论numpy.take在内部做什么,我都没有理由认为当索引更大时,为什么这个额外的“间接级别”应该更慢。

第三次更新:

根据@ hpaulj的评论,MyArray.take(0)MyArray[0]不相同,这是一个更正后的代码示例。 (我通过我的MATLAB直觉犯了这个错误,并且一度停止验证我的最小例子。我不想替换原来的例子,因为hpaulj的答案可能取决于它。)

import timeit
import numpy as np

for UseSplit in (True, False):
    if UseSplit:
        print("Using split")
        MyDoubleArray = np.random.rand(5000, 5000, 2)
        MyArray = np.split(MyDoubleArray, 2, axis=2)[0]
    else:
        print("Not using split")
        MyArray = np.random.rand(5000, 5000, 1)

    print((MyArray.size, MyArray.shape, MyArray.dtype, np.isfortran(MyArray)))


    NumpyTaking = MyArray.take(0)
    DirectIndexing = MyArray.item(0)
    assert (NumpyTaking == DirectIndexing)

    print("Take 1")
    print(timeit.timeit(lambda: MyArray.take(0), number=1))
    print("Index 1")
    print(timeit.timeit(lambda: MyArray.item(0), number=1))


    NumpyTaking = MyArray.take(0, axis=2)
    DirectIndexing = MyArray[:, :, 0]
    assert (NumpyTaking == DirectIndexing).all()

    print("Take many")
    print(timeit.timeit(lambda: MyArray.take(0, axis=2), number=1))
    print("Index many")
    print(timeit.timeit(lambda: MyArray[:, :, 0], number=1))

在我的(其他)系统上使用此输出:

Using split
(25000000, (5000, 5000, 1), dtype('float64'), False)
Take 1
0.2260607502818708
Index 1
2.1667747519799052e-05
Take many
0.44334302173084994
Index many
0.0005971935325195243
Not using split
(25000000, (5000, 5000, 1), dtype('float64'), False)
Take 1
2.851019410510247e-05
Index 1
2.0527339755549434e-05
Take many
0.13906132276656846
Index many
1.444516501325488e-05

2 个答案:

答案 0 :(得分:2)

这个答案有点冗长和复杂,但我认为关键点是,MyArray[0]是两个结构中的一个视图。 MyArray.take在第二种情况(split)中复制。 copy需要更长的时间。

这两项行动并不相同:

In [302]: MyArray = np.ones((10000, 10000, 1))
In [303]: MyArray[0].shape
Out[303]: (10000, 1)
In [304]: MyArray.take(0).shape
Out[304]: ()
带有take(默认值)的

axis=None会对数组进行ravels。指定轴将返回与MyArray[0,:,:]相同的内容:

In [305]: MyArray.take(0,axis=0).shape
Out[305]: (10000, 1)

使用ipython timeit(和numpy 1.14)

In [306]: timeit MyArray[0].shape
425 ns ± 7.03 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [307]: timeit MyArray.take(0).shape
1.25 µs ± 11.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [308]: timeit MyArray.take(0,axis=0).shape
10.6 µs ± 22.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

take速度慢得多,我感到有些惊讶,尽管我从来没有想过要成为速度工具的印象。相反,它对以下情况很方便:

In [311]: MyArray.take(0, axis=1).shape
Out[311]: (10000, 1)
In [313]: MyArray[:,0,:].shape
Out[313]: (10000, 1)

在代码中使用数字而不是冒号指定轴更容易。

  

但是,如果您需要沿给定轴的元素,则可以更容易使用。

当我通过拆分构建MyArray时,take时间会更糟糕

In [321]: timeit MyArray[0].shape
422 ns ± 5.54 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [322]: timeit MyArray.take(0).shape
713 ms ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [323]: timeit MyArray.take(0,axis=0).shape
705 ms ± 3.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

ravel是大部分额外时间。我认为take必须复制一份:

In [324]: timeit MyArray.ravel()
710 ms ± 19.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

构建步骤:np.ones((10000, 10000, 2))需要更长时间,我担心会出现内存错误。我正在使用ones而不是empty来确保数组在使用前已完全分配。

这表明内存管理问题使时间变得复杂。

数据缓冲区指针告诉我数组是否是视图:

In [334]: MyArray.__array_interface__['data']
Out[334]: (139737581203472, False)
In [335]: MyArray2.__array_interface__['data']
Out[335]: (139737581203472, False)

MyArray2就像您的MyTwoArrays。因此,拆分返回视图,而不是副本。

ravel必须在分案中制作副本:

In [336]: MyArray.ravel().__array_interface__['data']
Out[336]: (139739981209616, False)
In [337]: MyArray2.ravel().__array_interface__['data']
Out[337]: (139737581203472, False)

查看数据缓冲区的索引与take

In [343]: MyArray[0].__array_interface__['data']
Out[343]: (139737581203472, False)
In [344]: MyArray.take(0, axis=0).__array_interface__['data']
Out[344]: (34066048, False)
In [345]: MyArray.take(0).__array_interface__['data']
Out[345]: (33320032, False)

MyArray[0]仍然是一种观点,因此相对较快。

take上的{p> MyArrayaxisIn [346]: timeit MyArray.copy() 701 ms ± 1.87 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 的副本。

httpClient

我应该回去检查第一种情况下的复制,但是这次会话的内存负载正在拖累我的剩余计算。

答案 1 :(得分:1)

如果拆分矩阵而不复制数据(即视图),则需要为每个循环步骤创建一个需要解决的间接级别。这显然很慢,并解释了计算时间的大量增加。

唯一不会发生的情况是分割的后半部分是无效的,因为numpy视图总是删除单例列表的间接。如果使用以下行编辑代码,则可以观察到这一点:

take

现在第二个阵列的时间要快得多(虽然数据量仍然与以前相同)。

另一方面,如果您将一半数据复制到一个新阵列中,那么你就可以“平坦化”#34;这个间接,所以时间再次等同于非分裂数组。这是通常的时间与内存权衡

最后:为什么这个效果仅在take时可见,而不是花哨的索引?我只能猜测(应该通过分析来源确认)花哨的索引更聪明地检测视图间接,并且能够在执行实际循环之前重新组织链接结构。这两种方法显然没有共享相同的代码,因为在我上面的split-with-void示例中,只有{{1}}函数被加速,而不是花哨的索引...