为什么numpy.any在大型数组上这么慢?

我正在寻找确定是否大型阵列的最有效方法包含至少一个非零值。 乍看之下np.any看起来像显然是完成这项工作的工具,但在大型阵列上似乎出乎意料地缓慢。

考虑以下极端情况:

first = np.zeros(1E3,dtype=np.bool)
last = np.zeros(1E3,dtype=np.bool)

first[0] = True
last[-1] = True

# test 1
%timeit np.any(first)
>>> 100000 loops, best of 3: 6.36 us per loop

# test 2
%timeit np.any(last)
>>> 100000 loops, best of 3: 6.95 us per loop

至少np.all在这里似乎在做些明智的事情-如果非零值是数组中的第一个,应该没有必要考虑返回np.bool之前的其他任何项目,所以我希望测试1会稍微比测试更快2。

但是,当我们使数组更大时会发生什么呢?

first = np.zeros(1E9,dtype=np.bool)
last = np.zeros(1E9,dtype=np.bool)

first[0] = True
last[-1] = True

# test 3
%timeit np.any(first)
>>> 10 loops, best of 3: 21.6 ms per loop

# test 4
%timeit np.any(last)
>>> 1 loops, best of 3: 739 ms per loop

不出所料,测试4比测试3慢很多。但是,在测试3中np.all仍应只需要检查中的单个元素的值np.bool,以便知道它包含至少一个非零值。 为什么,那么,测试3是否比测试1慢得多?

编辑1:

我正在使用Numpy的开发版本(1.8.0.dev-e11cd9b),但是使用Numpy 1.7.1可以获得完全相同的计时结果。 我正在运行64位Linux,Python 2.7.4。 我的系统基本上处于闲置状态(我正在运行IPython会话,浏览器和文本编辑器),并且绝对不会出现问题。 我还将结果复制到另一台运行Numpy 1.7.1的计算机上。

编辑2:

使用Numpy 1.6.2时,测试1和3的时间均为〜1.85us,因此jorgeca表示在这方面,Numpy 1.6.2和1.7.1 1.7.0之间似乎存在一些性能下降。

编辑3:

遵循J.F. Sebastian和jorgeca的领导,我在零数组上使用np.all做了一些基准测试,这应该等效于在第一个元素为1的数组上调用np.bool

测试脚本:

import timeit
import numpy as np
print 'Numpy v%s' %np.version.full_version
stmt = "np.all(x)"
for ii in xrange(10):
    setup = "import numpy as np; x = np.zeros(%d,dtype=np.bool)" %(10**ii)
    timer = timeit.Timer(stmt,setup)
    n,r = 1,3
    t = np.min(timer.repeat(r,n))
    while t < 0.2:
        n *= 10
        t = np.min(timer.repeat(r,n))
    t /= n
    if t < 1E-3:
        timestr = "%1.3f us" %(t*1E6)
    elif t < 1:
        timestr = "%1.3f ms" %(t*1E3)
    else:
        timestr = "%1.3f s" %t
    print "Array size: 1E%i, %i loops, best of %i: %s/loop" %(ii,n,r,timestr)

结果:

Numpy v1.6.2
Array size: 1E0, 1000000 loops, best of 3: 1.738 us/loop
Array size: 1E1, 1000000 loops, best of 3: 1.845 us/loop
Array size: 1E2, 1000000 loops, best of 3: 1.862 us/loop
Array size: 1E3, 1000000 loops, best of 3: 1.858 us/loop
Array size: 1E4, 1000000 loops, best of 3: 1.864 us/loop
Array size: 1E5, 1000000 loops, best of 3: 1.882 us/loop
Array size: 1E6, 1000000 loops, best of 3: 1.866 us/loop
Array size: 1E7, 1000000 loops, best of 3: 1.853 us/loop
Array size: 1E8, 1000000 loops, best of 3: 1.860 us/loop
Array size: 1E9, 1000000 loops, best of 3: 1.854 us/loop

Numpy v1.7.0
Array size: 1E0, 100000 loops, best of 3: 5.881 us/loop
Array size: 1E1, 100000 loops, best of 3: 5.831 us/loop
Array size: 1E2, 100000 loops, best of 3: 5.924 us/loop
Array size: 1E3, 100000 loops, best of 3: 5.864 us/loop
Array size: 1E4, 100000 loops, best of 3: 5.997 us/loop
Array size: 1E5, 100000 loops, best of 3: 6.979 us/loop
Array size: 1E6, 100000 loops, best of 3: 17.196 us/loop
Array size: 1E7, 10000 loops, best of 3: 116.162 us/loop
Array size: 1E8, 1000 loops, best of 3: 1.112 ms/loop
Array size: 1E9, 100 loops, best of 3: 11.061 ms/loop

Numpy v1.7.1
Array size: 1E0, 100000 loops, best of 3: 6.216 us/loop
Array size: 1E1, 100000 loops, best of 3: 6.257 us/loop
Array size: 1E2, 100000 loops, best of 3: 6.318 us/loop
Array size: 1E3, 100000 loops, best of 3: 6.247 us/loop
Array size: 1E4, 100000 loops, best of 3: 6.492 us/loop
Array size: 1E5, 100000 loops, best of 3: 7.406 us/loop
Array size: 1E6, 100000 loops, best of 3: 17.426 us/loop
Array size: 1E7, 10000 loops, best of 3: 115.946 us/loop
Array size: 1E8, 1000 loops, best of 3: 1.102 ms/loop
Array size: 1E9, 100 loops, best of 3: 10.987 ms/loop

Numpy v1.8.0.dev-e11cd9b
Array size: 1E0, 100000 loops, best of 3: 6.357 us/loop
Array size: 1E1, 100000 loops, best of 3: 6.399 us/loop
Array size: 1E2, 100000 loops, best of 3: 6.425 us/loop
Array size: 1E3, 100000 loops, best of 3: 6.397 us/loop
Array size: 1E4, 100000 loops, best of 3: 6.596 us/loop
Array size: 1E5, 100000 loops, best of 3: 7.569 us/loop
Array size: 1E6, 100000 loops, best of 3: 17.445 us/loop
Array size: 1E7, 10000 loops, best of 3: 115.109 us/loop
Array size: 1E8, 1000 loops, best of 3: 1.094 ms/loop
Array size: 1E9, 100 loops, best of 3: 10.840 ms/loop

编辑4:

按照seberg的评论,我尝试使用np.all阵列而不是np.bool进行相同的测试。在这种情况下,随着数组大小的增加,Numpy 1.6.2也显示出速度下降:

Numpy v1.6.2
Array size: 1E0, 100000 loops, best of 3: 3.503 us/loop
Array size: 1E1, 100000 loops, best of 3: 3.597 us/loop
Array size: 1E2, 100000 loops, best of 3: 3.742 us/loop
Array size: 1E3, 100000 loops, best of 3: 4.745 us/loop
Array size: 1E4, 100000 loops, best of 3: 14.533 us/loop
Array size: 1E5, 10000 loops, best of 3: 112.463 us/loop
Array size: 1E6, 1000 loops, best of 3: 1.101 ms/loop
Array size: 1E7, 100 loops, best of 3: 11.724 ms/loop
Array size: 1E8, 10 loops, best of 3: 116.924 ms/loop
Array size: 1E9, 1 loops, best of 3: 1.168 s/loop

Numpy v1.7.1
Array size: 1E0, 100000 loops, best of 3: 6.548 us/loop
Array size: 1E1, 100000 loops, best of 3: 6.546 us/loop
Array size: 1E2, 100000 loops, best of 3: 6.804 us/loop
Array size: 1E3, 100000 loops, best of 3: 7.784 us/loop
Array size: 1E4, 100000 loops, best of 3: 17.946 us/loop
Array size: 1E5, 10000 loops, best of 3: 117.235 us/loop
Array size: 1E6, 1000 loops, best of 3: 1.096 ms/loop
Array size: 1E7, 100 loops, best of 3: 12.328 ms/loop
Array size: 1E8, 10 loops, best of 3: 118.431 ms/loop
Array size: 1E9, 1 loops, best of 3: 1.172 s/loop

为什么会发生这种情况? 与布尔型情况一样,np.all仍应只需要在返回前检查第一个元素,因此时间应保持不变。 数组大小。

ali_m asked 2020-08-09T14:47:33Z
1个解决方案
28 votes

正如评论中所猜到的,我可以确认数组的处理是按块完成的。 首先,我将向您展示代码中的内容,然后向您展示如何更改块大小以及这样做对基准的影响。

在Numpy源文件中的哪里找到简化处理

np.all(x)与x.all()相同。 all()真正调用np.core.umath.logical_and.reduce(x)。

如果您想深入了解numpy源,我将尝试引导您找到使用了缓冲区/块大小的方法。 我们将要查看的包含所有代码的文件夹是numpy / core / src / umath /。

ufunc_object.c中的PyUFunc_Reduce()是处理reduce的C函数。 在PyUFunc_Reduce()中,通过PyUFunc_GetPyValues()函数(ufunc_object.c)在某些全局词典中查找reduce的值来找到块或缓冲区的大小。 在我的计算机上并从开发分支进行编译,块大小为8192。reduce.c中的PyUFunc_ReduceWrapper()被调用以设置迭代器(步长等于块大小),并调用传入的循环函数 是ufunc_object.c中的reduce_loop()。

reduce_loop()基本上只使用迭代器,并为每个块调用另一个innerloop()函数。 innerloop函数位于loops.c.src中。 对于布尔数组和我们的all / logical_and案例,适当的innerloop函数是BOOL_logical_and。 您可以通过搜索BOOLEAN LOOPS找到合适的函数,然后找到它下面的第二个函数(由于此处使用了类似模板的编程,因此很难找到它)。 在那里,您会发现实际上每个块都发生了短路。

如何更改函数(以及任何/全部)中使用的缓冲区大小

您可以使用np.getbuffersize()获得块/缓冲区的大小。 对我来说,它返回8192,而无需手动设置它,与通过打印代码中的缓冲区大小找到的匹配。 您可以使用np.setbuffersize()更改块大小。

使用更大的缓冲区大小的结果

我将您的基准代码更改为以下代码:

import timeit
import numpy as np
print 'Numpy v%s' %np.version.full_version
stmt = "np.all(x)"
for ii in xrange(9):
    setup = "import numpy as np; x = np.zeros(%d,dtype=np.bool); np.setbufsize(%d)" %(10**ii, max(8192, min(10**ii, 10**7)))
    timer = timeit.Timer(stmt,setup)
    n,r = 1,3
    t = np.min(timer.repeat(r,n))
    while t < 0.2:
        n *= 10
        t = np.min(timer.repeat(r,n))
    t /= n
    if t < 1E-3:
        timestr = "%1.3f us" %(t*1E6)
    elif t < 1:
        timestr = "%1.3f ms" %(t*1E3)
    else:
        timestr = "%1.3f s" %t
    print "Array size: 1E%i, %i loops, best of %i: %s/loop" %(ii,n,r,timestr)

Numpy不喜欢缓冲区大小过小或太大,因此我确保它不会小于8192或大于1E7,因为Numpy不喜欢缓冲区大小为1E8。 否则,我将缓冲区大小设置为正在处理的数组的大小。 我只上了1E8,因为我的机器目前只有4GB的内存。 结果如下:

Numpy v1.8.0.dev-2a5c2c8
Array size: 1E0, 100000 loops, best of 3: 5.351 us/loop
Array size: 1E1, 100000 loops, best of 3: 5.390 us/loop
Array size: 1E2, 100000 loops, best of 3: 5.366 us/loop
Array size: 1E3, 100000 loops, best of 3: 5.360 us/loop
Array size: 1E4, 100000 loops, best of 3: 5.433 us/loop
Array size: 1E5, 100000 loops, best of 3: 5.400 us/loop
Array size: 1E6, 100000 loops, best of 3: 5.397 us/loop
Array size: 1E7, 100000 loops, best of 3: 5.381 us/loop
Array size: 1E8, 100000 loops, best of 3: 6.126 us/loop

在最后一个时间有一个小上升,因为由于缓冲区大小的限制,要处理多个块。

Justin Peel answered 2020-08-09T14:48:31Z
translate from https://stackoverflow.com:/questions/17128116/why-is-numpy-any-so-slow-over-large-arrays