原文: http://numba.pydata.org/numba-doc/latest/user/performance-tips.html
这是 Numba 中功能的简短指南,可以帮助您从代码中获得最佳性能。使用了两个例子,两者都完全是人为的,纯粹出于教学原因而存在,以激发讨论。第一个是三角恒等式cos(x)^2 + sin(x)^2
的计算,第二个是矢量的简单元素方形平方根,它是求和的减少。所有性能数字仅供参考,除非另有说明,否则选自在np.arange(1.e7)
输入的英特尔i7-4790
CPU(4 个硬件线程)上运行。
注意
实现高性能代码的一种合理有效的方法是使用实际数据分析运行的代码,并使用它来指导性能调优。这里提供的信息是为了展示功能,而不是作为规范指导!
一个常见的模式是用@jit
来装饰函数,因为这是 Numba 提供的最灵活的装饰器。 @jit
本质上包含两种编译模式,首先它将尝试在没有 Python 模式下编译装饰函数,如果失败,它将再次尝试使用对象模式编译函数。虽然在对象模式下使用循环可以提高性能,但是在无 python 模式下编译函数确实是获得良好性能的关键。为了使得只使用没有 python 模式,并且如果编译失败,则引发异常,可以使用装饰器@njit
和@jit(nopython=True)
(为方便起见,第一个是第二个的别名)。
虽然 NumPy 在矢量运算的使用方面已经形成了一个强有力的习惯,但 Numba 对循环也非常满意。对于熟悉 C 或 Fortran 的用户,以这种方式编写 Python 在 Numba 中可以正常工作(毕竟,LLVM 在编译 C 谱系语言时有很多用处)。例如:
@njit
def ident_np(x):
return np.cos(x) ** 2 + np.sin(x) ** 2
@njit
def ident_loops(x):
r = np.empty_like(x)
n = len(x)
for i in range(n):
r[i] = np.cos(x[i]) ** 2 + np.sin(x[i]) ** 2
return r
当用@njit
修饰时,上面以几乎相同的速度运行,没有装饰器,矢量化功能的速度提高了几个数量级。
功能名称 | @njit | 执行时间处理时间 |
---|---|---|
ident_np |
没有 | 0.581s |
ident_np |
是 | 0.659s |
ident_loops |
没有 | 25.2s |
ident_loops |
是 | 0.670s |
在某些类别的应用中,严格的 IEEE 754 合规性不那么重要。因此,可以放松一些数字严谨性,以获得额外的性能。在 Numba 中实现此行为的方法是使用fastmath
关键字参数:
@njit(fastmath=False)
def do_sum(A):
acc = 0.
# without fastmath, this loop must accumulate in strict order
for x in A:
acc += np.sqrt(x)
return acc
@njit(fastmath=True)
def do_sum_fast(A):
acc = 0.
# with fastmath, the reduction can be vectorized as floating point
# reassociation is permitted.
for x in A:
acc += np.sqrt(x)
return acc
功能名称 | 执行时间处理时间 |
---|---|
do_sum |
35.2 毫秒 |
do_sum_fast |
17.8 毫秒 |
如果代码包含可并行的操作(和支持),Numba 可以编译一个版本,它将在多个本机线程上并行运行(没有 GIL!)。这种并行化是自动执行的,只需添加parallel
关键字参数即可启用:
@njit(parallel=True)
def ident_parallel(A):
return np.cos(x) ** 2 + np.sin(x) ** 2
执行时间如下:
功能名称 | 执行时间处理时间 |
---|---|
ident_parallel |
112 毫秒 |
存在parallel=True
的此功能的执行速度约为 NumPy 等效值的 5 倍,是标准@njit
的 6 倍。
Numba 并行执行也支持显式并行循环声明,类似于 OpenMP。为了表明应该并行执行循环,应该使用numba.prange
函数,这个函数的行为类似于 Python range
,如果没有设置parallel=True
,它只是作为range
的别名。用prange
诱导的循环可用于令人尴尬的并行计算和减少。
重新考虑 reduce over sum 示例,假设无法按顺序累积总和是安全的,n
中的循环可以通过使用prange
来并行化。此外,在这种情况下可以毫无顾虑地添加fastmath=True
关键字参数,因为已经通过使用parallel=True
(因为每个线程计算部分和)已经进行了无序执行有效的假设。
@njit(parallel=True)
def do_sum_parallel(A):
# each thread can accumulate its own partial sum, and then a cross
# thread reduction is performed to obtain the result to return
n = len(A)
acc = 0.
for i in prange(n):
acc += np.sqrt(A[i])
return acc
@njit(parallel=True, fastmath=True)
def do_sum_parallel_fast(A):
n = len(A)
acc = 0.
for i in prange(n):
acc += np.sqrt(A[i])
return acc
执行时间如下,fastmath
再次提高性能。
功能名称 | 执行时间处理时间 |
---|---|
do_sum_parallel |
9.81 毫秒 |
do_sum_parallel_fast |
5.37 毫秒 |
英特尔提供了一个简短的矢量数学库(SVML),其中包含大量优化的超越函数,可用作编译器内在函数。如果环境中存在icc_rt
包(或者 SVML 库只是可定位的!),那么 Numba 会自动配置 LLVM 后端以尽可能使用 SVML 内部函数。 SVML 提供每个内在函数的高精度和低精度版本,并且使用的版本通过使用fastmath
关键字来确定。默认使用精度高于1 ULP
的高精度,但如果fastmath
设置为True
,则使用内在函数的低精度版本(4 ULP
内的答案)。
首先使用 conda 获取 SVML,例如:
conda install -c numba icc_rt
从上面重新运行身份函数示例ident_np
,使用@njit
和/或不使用 SVML 的各种选项组合,得到以下性能结果(输入大小np.arange(1.e8)
)。作为参考,仅使用 NumPy 在5.84s
中执行的功能:
@njit kwargs |
SVML | 执行时间处理时间 |
---|---|---|
None |
没有 | 5.95s |
None |
是 | 2.26s |
fastmath=True |
没有 | 5.97s |
fastmath=True |
是 | 1.8 秒 |
parallel=True |
没有 | 1.36s |
parallel=True |
是 | 0.624s |
parallel=True, fastmath=True |
没有 | 1.32s |
parallel=True, fastmath=True |
是 | 0.576s |
很明显,SVML 显着提高了该功能的性能。在不存在 SVML 的情况下fastmath
的影响是零,这是预期的,因为原始函数中没有任何东西可以从放宽数字严格性中受益。
Numba 在没有 Python 模式的情况下支持大多数numpy.linalg
。内部实现依赖于 LAPACK 和 BLAS 库来完成数值工作,并从 SciPy 获取必要函数的绑定。因此,要在 Numba 的numpy.linalg
函数中实现良好的性能,必须使用针对优化良好的 LAPACK / BLAS 库构建的 SciPy。在 Anaconda 发行版的情况下,SciPy 是针对英特尔的 MKL 构建的,该版本经过高度优化,因此 Numba 利用了这一性能。