为什么numpy.any在大型数组上如此缓慢?

我正在寻找最有效的方法来确定一个大数组是否至less包含一个非零值。 乍一看, 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 

至lessnp.any似乎正在做一些比较明智的事情 – 如果非零值是数组中的第一个,那么在返回True之前不需要考虑其他值,所以我预计testing1会比testing2。

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

 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 

正如所料,testing4比testing3慢很多。但是,在testing3中, np.any仍然只需要首先检查单个元素的值,以便知道它至less包含一个非零值。 那为什么testing3比testing1慢得多?

编辑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我得到的testing1和3的时间都是〜1.85us,所以jorgeca说在这方面Numpy 1.6.2和1.7.1 1.7.0之间似乎有一些性能的回归。

编辑3:

在JF Sebastian和jorgeca的领导下,我已经在一个零数组上使用np.all进行了一些基准testing,这应该等同于在第一个元素为1的数组上调用np.any

testing脚本:

 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.3fs" %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.float32数组而不是np.bool尝试了相同的testing。 在这种情况下,随着数组大小的增加,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在返回之前仍然只需要检查第一个元素,所以时间应该是wrt数组的大小。

正如已经在评论中猜到的那样,我可以确认数组的处理是以大块的方式完成的。 首先,我将向您展示代码中的内容,然后介绍如何更改块大小以及对基准testing的影响。

哪里可以findNumpy源文件中的缩小处理

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()函数。 内循环函数可以在loops.c.src中find。 对于布尔数组和我们所有/ logical_and的情况,适当的innerloop函数是BOOL_logical_and。 你可以通过searchBOOLEAN LOOPSfind正确的函数,然后它是下面的第二个函数(由于这里使用的模板编程很难find)。 在那里,你会发现实际上每个组块都有短路。

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

您可以使用np.getbuffersize()获取块/缓冲区大小。 对于我来说,返回8192而不是手动设置它,它与我在代码中打印出缓冲区大小所find的匹配。 您可以使用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.3fs" %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 

在上一次计时中有一个小的提升,因为由于缓冲区大小可能有多大的限制,正在处理多个块。