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

Posted

技术标签:

【中文标题】为啥 numpy.any 在大型数组上如此缓慢?【英文标题】:Why is numpy.any so slow over large arrays?为什么 numpy.any 在大型数组上如此缓慢? 【发布时间】:2013-06-12 06:16:39 【问题描述】:

我正在寻找最有效的方法来确定一个大数组是否 包含至少一个非零值。乍一看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.any 似乎在这里做了一些模糊的事情——如果 非零值是数组中的第一个,应该不需要考虑 返回True 之前的任何其他人,所以我希望测试 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.any 应该仍然只需要检查单个元素的值 first 以便知道它包含至少一个非零值。为什么, 那么,测试 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.any

测试脚本:

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.float32 数组而不是 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 在返回之前仍然只需要检查第一个元素,因此时间应该仍然是恒定的 w.r.t。数组大小。

【问题讨论】:

我无法在 numpy 1.6.2 中重现它:在这两种情况下,我首先得到 2 µs。您的计算机在这两种情况下都处于空闲/不交换状态吗? 好吧,我也可以在开发版中重现它,所以我猜在 1.6.2 和 1.7.1 之间存在性能回归。 the time is constant upto 1e4-1e5 items, then it starts to increase (almost linear 1e6-1e9)。 np.all() 中可能会针对需要与大小成比例的工作的大量项目触发一些优化。 我不会太在意它,因为这样的事情只会影响纯粹的布尔数组(老实说,你通常会先计算那些)。可能新机器总是将 ufunc 分块,因为它从来没有真正伤害过。请注意,开销很小,它仍然比这些时间的完整扫描快 60 倍。 也就是说,当不需要缓冲时,我猜它会将最内部的维度扩展到最大可能的大小,因此机器中可能存在一个错误,导致它无法做到这一点。如果你在乎,请阅读 nditer 代码,我认为它在某些地方会尝试扩展。 【参考方案1】:

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

在 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。调用 reduction.c 中的 PyUFunc_ReduceWrapper() 来设置迭代器(步幅等于块大小)并调用传入的循环函数,该函数是 ufunc_object.c 中的 reduce_loop()。

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

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

您可以使用 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

由于缓冲区大小的限制,有多个块正在处理,因此最后一次计时有小幅上升。

【讨论】:

以上是关于为啥 numpy.any 在大型数组上如此缓慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 python DataFrame 执行如此缓慢

为啥在索引 Parquet 文件上计算形状如此缓慢?

为啥我的 ember-cli 构建时间在 Windows 上如此缓慢?

为啥附加到调试器的运行如此缓慢?

为啥我的所见即所得编辑器在 vuejs 中运行如此缓慢?

为啥我使用多处理/多线程的函数在 for 循环中使用时如此缓慢,但在循环之外却没有?