由于我的程序需要快速索引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
模式下使用jitting,缓存和nogil
,我装饰了由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函数而言,所有索引功能都有轻微的加速,除了布尔掩码索引。简单的fancy indexing在这里表现最佳,但仍然比没有jitting的布尔掩码索引慢。
对于较大的数组,布尔掩码索引比其他方法慢得多,甚至比非jitted版本还要慢。另外三种方法的性能都相当不错,比非jitted版本快约15%。
对于我这种有许多不同大小数组的情况,使用numba的fancy indexing是最好的方法。也许其他人也能在这篇相当冗长的文章中找到一些有用的信息。
编辑:
很抱歉我忘记了我的问题,实际上我有一个问题。我在快下班时匆忙打字,完全忘记了它...
那么,您知道是否有比我测试过的更好更快的方法吗?使用Cython,我的计时介于Numba和Python之间。
由于索引数组预定义一次并在长迭代中不进行修改,因此任何预定义索引过程的方法都将是很棒的。为此,我考虑使用步幅。但我无法预定义自定义步幅集。是否可能使用步幅获得预定义视图进入内存?
编辑2:
我想我会将关于预定义常数索引数组的问题移动到一个更具体的新问题中,该问题将在相同值数组上使用(其中只有值发生变化而形状不变)进行数百万次迭代。这个问题太笼统了,也许我还表达了问题,我将尽快发布新问题的链接!
这是后续问题的链接。