【发布时间】:2018-02-12 23:20:44
【问题描述】:
因为对于我的程序,Numpy 数组的快速索引是非常必要的,而且考虑到性能,花哨的索引并没有很好的声誉,所以我决定进行一些测试。尤其是Numba 发展很快,我尝试了哪些方法与 numba 配合得很好。
作为输入,我一直在为我的小数组测试使用以下数组:
import numpy as np
import numba as nb
x = np.arange(0, 100, dtype=np.float64) # array to be indexed
idx = np.array((0, 4, 55, -1), dtype=np.int32) # fancy indexing array
bool_mask = np.zeros(x.shape, dtype=np.bool) # boolean indexing mask
bool_mask[idx] = True # set same elements as in idx True
y = np.zeros(idx.shape, dtype=np.float64) # output array
y_bool = np.zeros(bool_mask[bool_mask == True].shape, dtype=np.float64) #bool output array (only for convenience)
以及用于我的大型数组测试的以下数组(此处需要y_bool 来处理来自randint 的欺骗号码):
x = np.arange(0, 1000000, dtype=np.float64)
idx = np.random.randint(0, 1000000, size=int(1000000/50))
bool_mask = np.zeros(x.shape, dtype=np.bool)
bool_mask[idx] = True
y = np.zeros(idx.shape, dtype=np.float64)
y_bool = np.zeros(bool_mask[bool_mask == True].shape, dtype=np.float64)
这会在不使用 numba 的情况下产生以下时间:
%timeit x[idx]
#1.08 µs ± 21 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
#large arrays: 129 µs ± 3.45 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit x[bool_mask]
#482 ns ± 18.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
#large arrays: 621 µs ± 15.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit np.take(x, idx)
#2.27 µs ± 104 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
# large arrays: 112 µs ± 5.76 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit np.take(x, idx, out=y)
#2.65 µs ± 134 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
# large arrays: 134 µs ± 4.47 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit x.take(idx)
#919 ns ± 21.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 108 µs ± 1.71 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit x.take(idx, out=y)
#1.79 µs ± 40.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# larg arrays: 131 µs ± 2.92 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit np.compress(bool_mask, x)
#1.93 µs ± 95.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 618 µs ± 15.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit np.compress(bool_mask, x, out=y_bool)
#2.58 µs ± 167 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
# large arrays: 637 µs ± 9.88 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit x.compress(bool_mask)
#900 ns ± 82.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 628 µs ± 17.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit x.compress(bool_mask, out=y_bool)
#1.78 µs ± 59.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 628 µs ± 13.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit np.extract(bool_mask, x)
#5.29 µs ± 194 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
# large arrays: 641 µs ± 13 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
还有numba,在nopython-mode、caching和nogil中使用jitting,我修饰了索引的方式,numba支持:
@nb.jit(nopython=True, cache=True, nogil=True)
def fancy(x, idx):
x[idx]
@nb.jit(nopython=True, cache=True, nogil=True)
def fancy_bool(x, bool_mask):
x[bool_mask]
@nb.jit(nopython=True, cache=True, nogil=True)
def taker(x, idx):
np.take(x, idx)
@nb.jit(nopython=True, cache=True, nogil=True)
def ndtaker(x, idx):
x.take(idx)
对于小型和大型数组,这会产生以下结果:
%timeit fancy(x, idx)
#686 ns ± 25.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 84.7 µs ± 1.82 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit fancy_bool(x, bool_mask)
#845 ns ± 31 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 843 µs ± 14.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit taker(x, idx)
#814 ns ± 21.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 87 µs ± 1.52 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit ndtaker(x, idx)
#831 ns ± 24.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# large arrays: 85.4 µs ± 2.69 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
总结
虽然对于没有 numba 的 numpy,但显然小数组最好用布尔掩码索引(与 ndarray.take(idx) 相比大约是 2 倍),而对于较大的数组 ndarray.take(idx) 将表现最好,在这种情况下大约是 6 次比布尔索引更快。盈亏平衡点的数组大小约为 1000 单元格,索引数组大小约为 20 单元格。
对于具有1e5 元素和5e3 索引数组大小的数组,ndarray.take(idx) 将比布尔掩码索引快10 倍。因此,布尔索引似乎随着数组大小而显着减慢,但在达到某个数组大小阈值后会稍微赶上。
对于 numba jitted 函数,除了布尔掩码索引之外,所有索引函数都有一个小的加速。简单的花式索引在这里效果最好,但仍然比没有抖动的布尔掩码慢。
对于较大的数组,布尔掩码索引比其他方法慢很多,甚至比非 jitted 版本慢。其他三种方法的性能都非常好,比非抖动版本快 15% 左右。
对于我有许多不同大小的数组的情况,使用 numba 进行花式索引是最好的方法。也许其他人也可以在这篇相当长的帖子中找到一些有用的信息。
编辑:
对不起,我忘了问我的问题,事实上我有。我只是在工作日结束时快速输入这个并完全忘记了它......
那么,您知道比我测试的方法更好更快的方法吗?使用 Cython 我的时间介于 Numba 和 Python 之间。
由于索引数组是预先定义的,并且在长时间迭代中使用而无需更改,因此任何预定义索引过程的方式都会很棒。为此,我考虑过使用 strides。但我无法预先定义一组自定义的步幅。是否可以使用 strides 将预定义的视图放入内存中?
编辑 2:
我想我会把关于预定义常量索引数组的问题转移到一个新的更具体的问题上,这些数组将在相同的值数组(只有值改变但形状不改变)上进行几百万次迭代。这个问题太笼统了,也许我也提出了这个问题有点误导。我会在打开新问题后立即在此处发布链接!
Here is the link to the followup question.
【问题讨论】:
-
这里有什么问题?问一个真正的问题并自己回答不是更好吗?
-
Scotty,将您的问题更改为实际问题,然后将所有内容粘贴到自我回答中。如果您愿意,我将通过社区 wiki 将其粘贴,因此您可以在关闭(并删除)之前接受“不清楚您在问什么”
-
@DanielF 感谢您的提示!我在最后添加了一个问题!
标签: python performance numpy indexing numba