Numba ~5分钟指南

Numba 是一个适用于 Python 的即时(JIT)编译器,最适合使用 NumPy 数组和函数以及循环的代码。使用 Numba 最常见的方法是应用其装饰器集合到您的函数上,以指示 Numba 编译它们。当调用 Numba 装饰的函数时,它会“即时”编译成机器码以供执行,随后您的代码的全部或部分可以以原生机器码的速度运行!

Numba 开箱即用,支持以下内容:

  • 操作系统:Windows (64 位)、OSX、Linux (64 位)。非官方支持 *BSD。

  • 架构:x86、x86_64、ppc64le、armv8l (aarch64)、M1/Arm64。

  • GPU:Nvidia CUDA。

  • CPython

  • NumPy 1.24 - 1.26

如何获取?

Numba 可作为 conda 包,用于 Anaconda Python 发行版

$ conda install numba

Numba 也有 wheel 包可用

$ pip install numba

Numba 也可以从源代码编译,但我们不建议初次使用 Numba 的用户这样做。

Numba 通常作为一个核心包使用,因此其依赖项被限制到最低限度,但是,可以安装额外的包以提供附加功能,如下所示:

  • scipy - 启用编译 numpy.linalg 函数的支持。

  • colorama - 启用回溯/错误消息中的颜色高亮支持。

  • pyyaml - 启用通过 YAML 配置文件配置 Numba。

  • intel-cmplr-lib-rt - 允许使用 Intel SVML(高性能短向量数学库,仅限 x86_64)。安装说明请参见性能提示

Numba 适用于我的代码吗?

这取决于您的代码是什么样子,如果您的代码是数值导向(进行了大量数学运算),大量使用 NumPy 和/或有大量循环,那么 Numba 通常是一个不错的选择。在这些示例中,我们将应用 Numba 最基本的 JIT 装饰器 @jit,尝试加速一些函数,以演示哪些效果好,哪些效果不好。

Numba 适用于以下类型的代码:

from numba import jit
import numpy as np

x = np.arange(100).reshape(10, 10)

@jit
def go_fast(a): # Function is compiled to machine code when called the first time
    trace = 0.0
    for i in range(a.shape[0]):   # Numba likes loops
        trace += np.tanh(a[i, i]) # Numba likes NumPy functions
    return a + trace              # Numba likes NumPy broadcasting

print(go_fast(x))

对于以下类型的代码,它不会很好地工作,甚至根本不起作用:

from numba import jit
import pandas as pd

x = {'a': [1, 2, 3], 'b': [20, 30, 40]}

@jit(forceobj=True, looplift=True) # Need to use object mode, try and compile loops!
def use_pandas(a): # Function will not benefit from Numba jit
    df = pd.DataFrame.from_dict(a) # Numba doesn't know about pd.DataFrame
    df += 1                        # Numba doesn't understand what this is
    return df.cov()                # or this!

print(use_pandas(x))

请注意,Numba 不理解 Pandas,因此 Numba 只会通过解释器运行此代码,但会增加 Numba 内部开销!

什么是 object mode

Numba 的 @jit 装饰器主要以两种编译模式运行:nopython 模式和 object 模式。在上面的 go_fast 示例中,@jit 装饰器默认以 nopython 模式运行。nopython 编译模式的行为是基本上将装饰函数编译为完全不依赖 Python 解释器运行。这是使用 Numba jit 装饰器的推荐和最佳实践方式,因为它能带来最佳性能。

如果 nopython 模式的编译失败,Numba 可以使用 object mode 进行编译。这可以通过在 @jit 装饰器中使用 forceobj=True 关键字参数来实现(如上面的 use_pandas 示例所示)。在此模式下,Numba 会假定所有内容都是 Python 对象并编译函数,本质上是在解释器中运行代码。指定 looplift=True 可能会比纯 object mode 获得一些性能提升,因为 Numba 会尝试将循环编译成机器码运行的函数,而代码的其余部分则在解释器中运行。为了获得最佳性能,一般情况下请避免使用 object mode 模式!

如何衡量 Numba 的性能?

首先,请记住 Numba 必须为给定的参数类型编译您的函数,然后才能执行您的函数的机器码版本。这需要时间。但是,一旦编译完成,Numba 会缓存您的函数特定参数类型的机器码版本。如果再次使用相同的类型调用,它可以重用缓存的版本,而无需再次编译。

衡量性能时一个非常常见的错误是,没有考虑到上述行为,并使用一个简单的计时器对代码进行一次计时,该计时器包含了函数编译所需的时间。

例如

from numba import jit
import numpy as np
import time

x = np.arange(100).reshape(10, 10)

@jit(nopython=True)
def go_fast(a): # Function is compiled and runs in machine code
    trace = 0.0
    for i in range(a.shape[0]):
        trace += np.tanh(a[i, i])
    return a + trace

# DO NOT REPORT THIS... COMPILATION TIME IS INCLUDED IN THE EXECUTION TIME!
start = time.perf_counter()
go_fast(x)
end = time.perf_counter()
print("Elapsed (with compilation) = {}s".format((end - start)))

# NOW THE FUNCTION IS COMPILED, RE-TIME IT EXECUTING FROM CACHE
start = time.perf_counter()
go_fast(x)
end = time.perf_counter()
print("Elapsed (after compilation) = {}s".format((end - start)))

例如,这会打印

Elapsed (with compilation) = 0.33030009269714355s
Elapsed (after compilation) = 6.67572021484375e-06s

衡量 Numba JIT 对代码影响的好方法是使用 timeit 模块函数进行计时;这些函数会测量多次执行迭代,因此可以适应首次执行中的编译时间。

另外,如果编译时间是一个问题,Numba JIT 支持已编译函数的磁盘缓存,并且还具有预先(Ahead-Of-Time)编译模式。

它有多快?

假设 Numba 可以在 nopython 模式下运行,或者至少编译一些循环,它将针对您的特定 CPU 进行编译。加速效果因应用而异,但可以达到一到两个数量级。Numba 有一个性能指南,涵盖了获得额外性能的常见选项。

Numba 如何工作?

Numba 读取装饰函数的 Python 字节码,并将其与函数输入参数的类型信息结合起来。它分析和优化您的代码,最后使用 LLVM 编译器库生成您的函数的机器码版本,该版本是根据您的 CPU 能力量身定制的。然后,每次调用您的函数时都会使用这个编译过的版本。

其他感兴趣的事项:

Numba 有相当多的装饰器,我们已经看到了 @jit,但还有:

  • @njit - 这是 @jit(nopython=True) 的别名,因为它太常用了!

  • @vectorize - 生成 NumPy ufunc(支持所有 ufunc 方法)。文档在此

  • @guvectorize - 生成 NumPy 广义 ufunc文档在此

  • @stencil - 声明一个函数作为类似模板操作的内核。文档在此

  • @jitclass - 用于支持 JIT 的类。文档在此

  • @cfunc - 声明一个函数用作原生回调(供 C/C++ 等调用)。文档在此

  • @overload - 注册您自己的函数实现在 nopython 模式下使用,例如 @overload(scipy.special.j0)文档在此

某些装饰器中提供的额外选项

ctypes/cffi/cython 互操作性

  • cffi - 在 nopython 模式下支持调用 CFFI 函数。

  • ctypes - 在 nopython 模式下支持调用 ctypes 封装的函数。

  • Cython 导出的函数可调用

GPU 目标:

Numba 可以针对 Nvidia CUDA GPU。您可以用纯 Python 编写内核,并让 Numba 处理计算和数据移动(或显式执行)。点击此处查看 Numba 关于 CUDA 的文档。