您的总结并不完全正确,您已经使用不同大小的数组进行了测试,但是您没有做的一件事就是更改索引元素的数量。
我将其限制为纯索引,并省略了
take(实际上是整数数组索引)
compress和
extract(因为它们实际上是布尔数组索引)。这些的唯一区别是恒定因素。对这些方法的常数因子
take和
compress将小于开销为numpy的功能
np.take和
np.compress否则的影响可以忽略不计的合理大小的数组。
让我用不同的数字表示它:
# ~ every 500th elementx = np.arange(0, 1000000, dtype=np.float64)idx = np.random.randint(0, 1000000, size=int(1000000/500)) # changed the ratio!bool_mask = np.zeros(x.shape, dtype=np.bool)bool_mask[idx] = True%timeit x[idx]# 51.6 µs ± 2.02 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)%timeit x[bool_mask]# 1.03 ms ± 37.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)# ~ every 50th elementidx = np.random.randint(0, 1000000, size=int(1000000/50)) # changed the ratio!bool_mask = np.zeros(x.shape, dtype=np.bool)bool_mask[idx] = True%timeit x[idx]# 1.46 ms ± 55.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)%timeit x[bool_mask]# 2.69 ms ± 154 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)# ~ every 5th elementidx = np.random.randint(0, 1000000, size=int(1000000/5)) # changed the ratio!bool_mask = np.zeros(x.shape, dtype=np.bool)bool_mask[idx] = True%timeit x[idx]# 14.9 ms ± 495 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)%timeit x[bool_mask]# 8.31 ms ± 181 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
那么这里发生了什么?很简单:整数数组索引只需要访问与索引数组中的值一样多的元素。这意味着如果匹配很少,那么如果索引很多,将会非常快但是很慢。但是,布尔数组索引始终需要遍历整个布尔数组并检查“真”值。这意味着该数组应该大致“恒定”。
但是,等等,对于布尔数组来说,它并不是真正恒定的,为什么整数数组索引需要比布尔数组索引花费更长的时间(最后一种情况),即使它必须处理的元素减少约5倍?
那就是它变得更加复杂的地方。在这种情况下,布尔数组
True位于随机位置,这意味着它将经受 分支预测失败
。如果
True并且
False发生次数相等,但发生在随机位置,则发生这些事件的可能性更大。这就是为什么布尔数组索引越来越慢-
因为比
True来
False得到更加平等,从而更加“随机”。如果有更多
True的,结果数组也会更大,这也会消耗更多的时间。
作为该分支预测事物的示例,请以以下示例为例(可能因不同的系统/编译器而异):
bool_mask = np.zeros(x.shape, dtype=np.bool)bool_mask[:1000000//2] = True # first half True, second half False%timeit x[bool_mask]# 5.92 ms ± 118 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)bool_mask = np.zeros(x.shape, dtype=np.bool)bool_mask[::2] = True # True and False alternating%timeit x[bool_mask]# 16.6 ms ± 361 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)bool_mask = np.zeros(x.shape, dtype=np.bool)bool_mask[::2] = Truenp.random.shuffle(bool_mask) # shuffled%timeit x[bool_mask]# 18.2 ms ± 325 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
因此,即使布尔掩码包含相同数量的s
,
Trueand的分布
False也会严重影响运行时间
True。
compress-functions可以看到相同的效果。
对于整数数组索引(同样
np.take),将看到另一个效果: cache locality
。您的情况下的索引是随机分布的,因此您的计算机必须执行很多“ RAM”到“处理器缓存”的加载,因为两个索引彼此之间不太可能相互接近。
比较一下:
idx = np.random.randint(0, 1000000, size=int(1000000/5))%timeit x[idx]# 15.6 ms ± 703 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)idx = np.random.randint(0, 1000000, size=int(1000000/5))idx = np.sort(idx) # sort them%timeit x[idx]# 4.33 ms ± 366 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
通过对索引进行排序,下一个值将已经存在于高速缓存中的机会大大增加,这可能导致巨大的加速。如果您知道索引将被排序,那么这是一个非常重要的因素(例如,如果索引是由
np.where它们创建的,则
np.where对索引进行排序特别有效)。
因此,这并不取决于整数数组索引对于较小的数组而言较慢,而对于较大的数组而言则取决于更多因素。两者都有其用例,并且根据情况,它们可能(相当)快于另一种。
让我也谈一谈numba函数。首先是一些一般性的陈述:
cache
不会有所不同,只是避免重新编译函数。在交互式环境中,这实际上是没有用的。但是,如果将功能打包在模块中会更快。nogil
本身不会提供任何速度提升。如果在不同的线程中调用它将更快,因为每个函数执行可以释放GIL,然后多个调用可以并行运行。
否则我不知道numba如何有效地实现这些功能,但是当您在numba中使用NumPy功能时,它可能会变慢或变快-
但即使速度更快,它也不会变快(也许对于小型阵列而言)。因为如果可以更快地进行开发,NumPy开发人员也会实施它。我的经验法则是:如果可以使用NumPy(向量化)进行操作,请不要理会numba。只有当您无法使用向量化NumPy函数或NumPy做到这一点时,它将使用过多的临时数组,然后numba才会发光!



