为什么numpy.absolute()这么慢?

时间:2017-10-01 13:00:08

标签: numpy optimization addition absolute-value

我需要优化一个大量使用计算L1矢量范数的脚本。 我们知道在这种情况下L1范数只是绝对值的总和。当计算numpy在这个任务中的速度有多快时,我发现了一些奇怪的东西:添加所有向量元素比获取向量的每个元素的绝对值快大约3倍。这是一个令人惊讶的结果,因为与获取绝对值相比,加法非常复杂,绝对值只需要数据块的每第32位归零(假设为float32)。

为什么这个加法比简单的按位运算快3倍?

import numpy as np

a = np.random.rand(10000000)

%timeit np.sum(a)
13.9 ms ± 87.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit np.abs(a)
41.2 ms ± 92.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

2 个答案:

答案 0 :(得分:3)

这里有几点需要考虑。 sum返回标量abs返回一个数组。因此,即使添加两个数字并且采用绝对速度相同,速度abs也会更慢,因为它需要创建数组。它必须处理两倍的元素(从输入读取+写入输出)。

因此,您无法从这些时间推断出添加速度与按位操作的速度。

然而,您可以检查是否更快地向数组的每个值添加内容而不是取每个值的绝对值

%timeit a + 0.1
9 ms ± 155 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit abs(a)
9.98 ms ± 532 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

或者比较总和+内存分配与采用绝对值

%timeit np.full_like(a, 1); np.sum(a)
13.4 ms ± 358 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit abs(a) 
9.64 ms ± 320 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

万一你想要更快地计算规范,你可以尝试numba(或Cython,或自己编写C或Fortran例程),这样就可以避免任何内存分配:

import numba as nb

@nb.njit
def sum_of_abs(arr):
    sum_ = 0
    for item in arr:
        sum_ += abs(item)
    return sum_

sum_of_abs(a)  # one call for the jitter to kick in
%timeit sum_of_abs(a)
# 2.44 ms ± 315 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

答案 1 :(得分:2)

np.sum返回一个标量。 np.abs返回一个大小相同的新数组。为这个新阵列分配内存是这里花费最多的时间。比较

>>> timeit("np.abs(a)", "import numpy as np; a = np.random.rand(10000000)", number=100)
3.565487278989167
>>> timeit("np.abs(a, out=a)", "import numpy as np; a = np.random.rand(10000000)", number=100)
0.9392949139873963

参数out=a告诉NumPy将结果放在同一个数组a中,在那里覆盖旧数据。因此加速。

总和仍然稍快:

>>> timeit("np.sum(a)", "import numpy as np; a = np.random.rand(10000000)", number=100)
0.6874654769926565

但它不需要那么多 write 内存访问。

如果您不想覆盖a,那么为abs的输出提供另一个数组是可能的,假设您必须重复采用相同类型和大小的abs数组。

b = np.empty_like(a)   # done once, outside the loop
np.abs(a, out=b)
np.sum(b)

在约np.linalg(a, 1)

的一半时间内运行

作为参考,np.linalg计算L1范数为

add.reduce(abs(x), axis=axis, keepdims=keepdims)

涉及为新数组abs(x)分配内存。

理想情况下,有一种方法可以计算所有绝对值(或另一个" ufunc"的结果)的总和(或最大值或最小值),而无需将所有输出移动到RAM然后检索它总和/最大/最小值。在NumPy回购中有一些讨论,最近一次是在add a max_abs ufunc,但它还没有达到实施。

ufunc.reduce方法可用于具有两个输入的函数,例如addlogaddexp,但没有addabs函数(x, y : x+abs(y))可以减少。