1.13。性能提示

原文: 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 个硬件线程)上运行。

注意

实现高性能代码的一种合理有效的方法是使用实​​际数据分析运行的代码,并使用它来指导性能调优。这里提供的信息是为了展示功能,而不是作为规范指导!

1.13.1。没有 Python 模式与对象模式

一个常见的模式是用@jit来装饰函数,因为这是 Numba 提供的最灵活的装饰器。 @jit本质上包含两种编译模式,首先它将尝试在没有 Python 模式下编译装饰函数,如果失败,它将再次尝试使用对象模式编译函数。虽然在对象模式下使用循环可以提高性能,但是在无 python 模式下编译函数确实是获得良好性能的关键。为了使得只使用没有 python 模式,并且如果编译失败,则引发异常,可以使用装饰器@njit@jit(nopython=True)(为方便起见,第一个是第二个的别名)。

1.13.2。循环

虽然 NumPy 在矢量运算的使用方面已经形成了一个强有力的习惯,但 Numba 对循环也非常满意。对于熟悉 C 或 Fortran 的用户,以这种方式编写 Python 在 Numba 中可以正常工作(毕竟,LLVM 在编译 C 谱系语言时有很多用处)。例如:

  1. @njit
  2. def ident_np(x):
  3. return np.cos(x) ** 2 + np.sin(x) ** 2
  4. @njit
  5. def ident_loops(x):
  6. r = np.empty_like(x)
  7. n = len(x)
  8. for i in range(n):
  9. r[i] = np.cos(x[i]) ** 2 + np.sin(x[i]) ** 2
  10. return r

当用@njit修饰时,上面以几乎相同的速度运行,没有装饰器,矢量化功能的速度提高了几个数量级。

功能名称 @njit 执行时间处理时间
ident_np 没有 0.581s
ident_np 0.659s
ident_loops 没有 25.2s
ident_loops 0.670s

1.13.3。 Fastmath

在某些类别的应用中,严格的 IEEE 754 合规性不那么重要。因此,可以放松一些数字严谨性,以获得额外的性能。在 Numba 中实现此行为的方法是使用fastmath关键字参数:

  1. @njit(fastmath=False)
  2. def do_sum(A):
  3. acc = 0.
  4. # without fastmath, this loop must accumulate in strict order
  5. for x in A:
  6. acc += np.sqrt(x)
  7. return acc
  8. @njit(fastmath=True)
  9. def do_sum_fast(A):
  10. acc = 0.
  11. # with fastmath, the reduction can be vectorized as floating point
  12. # reassociation is permitted.
  13. for x in A:
  14. acc += np.sqrt(x)
  15. return acc
功能名称 执行时间处理时间
do_sum 35.2 毫秒
do_sum_fast 17.8 毫秒

1.13.4。并行=真

如果代码包含可并行的操作(和支持),Numba 可以编译一个版本,它将在多个本机线程上并行运行(没有 GIL!)。这种并行化是自动执行的,只需添加parallel关键字参数即可启用:

  1. @njit(parallel=True)
  2. def ident_parallel(A):
  3. 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(因为每个线程计算部分和)已经进行了无序执行有效的假设。

  1. @njit(parallel=True)
  2. def do_sum_parallel(A):
  3. # each thread can accumulate its own partial sum, and then a cross
  4. # thread reduction is performed to obtain the result to return
  5. n = len(A)
  6. acc = 0.
  7. for i in prange(n):
  8. acc += np.sqrt(A[i])
  9. return acc
  10. @njit(parallel=True, fastmath=True)
  11. def do_sum_parallel_fast(A):
  12. n = len(A)
  13. acc = 0.
  14. for i in prange(n):
  15. acc += np.sqrt(A[i])
  16. return acc

执行时间如下,fastmath再次提高性能。

功能名称 执行时间处理时间
do_sum_parallel 9.81 毫秒
do_sum_parallel_fast 5.37 毫秒

1.13.5。英特尔 SVML

英特尔提供了一个简短的矢量数学库(SVML),其中包含大量优化的超越函数,可用作编译器内在函数。如果环境中存在icc_rt包(或者 SVML 库只是可定位的!),那么 Numba 会自动配置 LLVM 后端以尽可能使用 SVML 内部函数。 SVML 提供每个内在函数的高精度和低精度版本,并且使用的版本通过使用fastmath关键字来确定。默认使用精度高于1 ULP的高精度,但如果fastmath设置为True,则使用内在函数的低精度版本(4 ULP内的答案)。

首先使用 conda 获取 SVML,例如:

  1. 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的影响是零,这是预期的,因为原始函数中没有任何东西可以从放宽数字严格性中受益。

1.13.6。线性代数

Numba 在没有 Python 模式的情况下支持大多数numpy.linalg。内部实现依赖于 LAPACK 和 BLAS 库来完成数值工作,并从 SciPy 获取必要函数的绑定。因此,要在 Numba 的numpy.linalg函数中实现良好的性能,必须使用针对优化良好的 LAPACK / BLAS 库构建的 SciPy。在 Anaconda 发行版的情况下,SciPy 是针对英特尔的 MKL 构建的,该版本经过高度优化,因此 Numba 利用了这一性能。