了解Numba性能差异



我正试图通过使用算法的各种numba实现来理解我所看到的性能差异。特别是,我希望下面的func1d是最快的实现,因为它是唯一不复制数据的算法,但从我的时间来看,func1b似乎是最快。

import numpy
import numba

def func1a(data, a, b, c):
# pure numpy
return a * (1 + numpy.tanh((data / b) - c))

@numba.njit(fastmath=True)
def func1b(data, a, b, c):
new_data = a * (1 + numpy.tanh((data / b) - c))
return new_data

@numba.njit(fastmath=True)
def func1c(data, a, b, c):
new_data = numpy.empty(data.shape)
for i in range(new_data.shape[0]):
for j in range(new_data.shape[1]):
new_data[i, j] = a * (1 + numpy.tanh((data[i, j] / b) - c)) 
return new_data

@numba.njit(fastmath=True)
def func1d(data, a, b, c):
for i in range(data.shape[0]):
for j in range(data.shape[1]):
data[i, j] = a * (1 + numpy.tanh((data[i, j] / b) - c)) 
return data

测试内存复制的助手功能

def get_data_base(arr):
"""For a given NumPy array, find the base array
that owns the actual data.

https://ipython-books.github.io/45-understanding-the-internals-of-numpy-to-avoid-unnecessary-array-copying/
"""
base = arr
while isinstance(base.base, numpy.ndarray):
base = base.base
return base

def arrays_share_data(x, y):
return get_data_base(x) is get_data_base(y)

def test_share(func):
data = data = numpy.random.randn(100, 3)
print(arrays_share_data(data, func(data, 0.5, 2.5, 2.5)))

定时

# force compiling
data = numpy.random.randn(10_000, 300)
_ = func1a(data, 0.5, 2.5, 2.5)
_ = func1b(data, 0.5, 2.5, 2.5)
_ = func1c(data, 0.5, 2.5, 2.5)
_ = func1d(data, 0.5, 2.5, 2.5)
data = numpy.random.randn(10_000, 300)
%timeit func1a(data, 0.5, 2.5, 2.5)
%timeit func1b(data, 0.5, 2.5, 2.5)
%timeit func1c(data, 0.5, 2.5, 2.5)
%timeit func1d(data, 0.5, 2.5, 2.5)
67.2 ms ± 230 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
13 ms ± 10.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
69.8 ms ± 60.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
69.8 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

测试哪些实现复制内存

test_share(func1a)
test_share(func1b)
test_share(func1c)
test_share(func1d)
False
False
False
True

这里,数据的复制并没有起到很大的作用:瓶颈是如何快速评估tanh-函数。有很多算法:有些更快,有些更慢,有些更精确,有些更少。

不同的numpy发行版使用不同的tanh-函数实现,例如,它可以是来自mkl/vml的实现,也可以是来自gnu数学库的实现。

根据numba的版本,还可以使用mkl/svml实现或gnu数学库。

查看内部的最简单方法是使用探查器,例如perf

对于我机器上的numpy版本,我得到:

>>> perf record python run.py
>>> perf report
Overhead  Command  Shared Object                                      Symbol                                  
46,73%  python   libm-2.23.so                                       [.] __expm1
24,24%  python   libm-2.23.so                                       [.] __tanh
4,89%  python   _multiarray_umath.cpython-37m-x86_64-linux-gnu.so  [.] sse2_binary_scalar2_divide_DOUBLE
3,59%  python   [unknown]                                          [k] 0xffffffff8140290c

可以看到,numpy使用慢速gnu数学库(libm(功能。

对于我得到的numba函数:

53,98%  python   libsvml.so                                         [.] __svml_tanh4_e9
3,60%  python   [unknown]                                          [k] 0xffffffff81831c57
2,79%  python   python3.7                                          [.] _PyEval_EvalFrameDefault

这意味着使用了快速mkl/svml功能。

这(几乎(就是它的全部。


正如@user2640045正确指出的那样,由于创建临时数组而导致的额外缓存未命中将损害numpy性能。

然而,缓存未命中的作用不如tanh:的计算大

%timeit func1a(data, 0.5, 2.5, 2.5)  # 91.5 ms ± 2.88 ms per loop 
%timeit numpy.tanh(data)             # 76.1 ms ± 539 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

即临时对象的创建约占运行时间的20%。


FWIW,同样对于带有手写循环的版本,我的numba版本(0.50.1(能够向量化并调用mkl/svml功能。如果对于其他版本,这种情况没有发生——numba将返回到gnu数学库功能,即您的机器上似乎正在发生的事情。


run.py:的列表

import numpy
# TODO: define func1b for checking numba
def func1a(data, a, b, c):
# pure numpy
return a * (1 + numpy.tanh((data / b) - c))

data = numpy.random.randn(10_000, 300)
for _ in range(100):
func1a(data, 0.5, 2.5, 2.5)

性能差异不在tanh函数的评估中

我一定不同意@ead的观点。让我们暂时假设

主要性能差异在于tanh函数的评估

那么,人们可以预期,从numpynumba仅运行tanh,并使用快速数学运算,就会显示出这种速度差。

def func_a(data):
return np.tanh(data)
@nb.njit(fastmath=True)
def func_b(data):
new_data = np.tanh(data)
return new_data
data = np.random.randn(10_000, 300)
%timeit func_a(data)
%timeit func_b(data)

然而,在我的机器上,上面的代码几乎没有显示出性能上的差异。

15.7 ms ± 129 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
15.8 ms ± 82 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

NumExpr上的短绕行

我尝试了您代码的NumExpr版本。但在惊讶于它的运行速度快了7倍之前,你应该记住,它使用了我机器上所有的10个核心。在允许numba也并行运行并对其进行一点优化之后,性能优势很小,但与3.87 ms相比,2.56 ms仍然存在。请参阅下面的代码。

@nb.njit(fastmath=True)
def func_a(data):
new_data = a * (1 + np.tanh((data / b) - c))
return new_data
@nb.njit(fastmath=True, parallel=True)
def func_b(data):
new_data = a * (1 + np.tanh((data / b) - c))
return new_data
@nb.njit(fastmath=True, parallel=True)
def func_c(data):
for i in nb.prange(data.shape[0]):
for j in range(data.shape[1]):
data[i, j] = a * (1 + np.tanh((data[i, j] / b) - c)) 
return data
def func_d(data):
return ne.evaluate('a * (1 + tanh((data / b) - c))')
data = np.random.randn(10_000, 300)
%timeit func_a(data)
%timeit func_b(data)
%timeit func_c(data)
%timeit func_d(data)
17.4 ms ± 146 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
4.31 ms ± 193 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.87 ms ± 152 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.56 ms ± 104 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

实际解释

numba相比,NumExpr节省了约34%的时间,这是很好的,但更好的是,它们有一个简洁的解释,为什么它们比numpy更快。我确信这也适用于numba

从NumExpr github页面:

NumExpr获得比NumPy更好性能的主要原因是它避免了为中间结果分配内存。这提高了缓存利用率并减少了中的内存访问全体的

所以

a * (1 + numpy.tanh((data / b) - c))

速度较慢,因为它需要执行许多步骤才能产生中间结果。

最新更新