什么?Python太慢?试试Numba库吧!
官方文档
官方文档入口
有需要的小伙伴请点入享用
Python编译过程和执行原理
本节参考了传送门1和传送门2
C/C++之类的编译性语言编写的程序,将源文件转换成计算机使用的机器语言,经过链接器链接之后形成了二进制的可执行文件。运行该程序的时候,就可以把二进制程序从硬盘载入到内存中并运行。而在Python作为解释型语言,没有编译这一步,而是由解释器将源代码转换为字节码,然后再由解释器来执行这些字节码,因此Python中不用担心程序的编译、库的链接加载等问题。
Python解释器(如CPython、IPython、PyPy、Jython、IronPython)执行Python代码的四个过程
- 词法分析
检查关键字等是否正确 - 语法分析
语句格式是否正确 - 生成字节码
生成.pyc文件(PyCodeObject对象)。在编译代码的过程中,首先会将代码中的函数、类等对象分类处理,然后生成字节码文件。 - 执行
Python解释器对字节码进行解释,将每一行的字节码解释成CPU可以直接识别的机器码,执行
常见的cpython解释器是用c语言的方式来解释字节码的,
而numba则是使用LLVM编译技术来解释字节码的。
我们之前写的CPython本质上还是通过C的编译器来替换掉CPython底层的复杂代码从而实现了加速(比如Python的动态类型,涉及到一大堆的类型检查、多态、溢出检查等耗时非常多,但是如果使用CPython的静态类型就没有这么多麻烦的问题),
而numba的思路则不太一样,numba是在一个叫做LLVM的编译器上进行编译,Numba将Python字节码转换为LLVM中间表示(IR),请注意,LLVM IR是一种低级编程语言,与汇编语法类似,与Python无关。
Numba简介
Numba是Python的一个即时编译器,它最适合使用NumPy数组、函数和循环的代码。使用Numba最常见的方式是通过它的装饰器集合,这些装饰器可以应用到您的函数中,以指示Numba编译它们。当调用一个Numba修饰函数时,它被编译成机器代码以便及时执行,并且您的全部或部分代码随后可以以本机机器代码的速度运行。
Numba在何时是有效的
这取决于您的代码是什么样子的,如果您的代码是面向数字的(做了很多数学工作),经常使用NumPy和/或有很多循环,那么Numba通常是一个不错的选择。在这些示例中,我们将应用最基本的Numba的JIT装饰器@jit来尝试加速一些函数,以演示哪些工作正常,哪些工作不正常。
对于如下代码,Numba很有有效
from numba import jit
import numpy as np
x = np.arange(100).reshape(10, 10)
@jit(nopython=True) # Set "nopython" mode for best performance, equivalent to @njit
def go_fast(a): # Function is compiled to machine code when called the first time
trace = 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))
而对于如下代码,Numba作用不大
from numba import jit
import pandas as pd
x = {'a': [1, 2, 3], 'b': [20, 30, 40]}
@jit
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))
@jit装饰器
@numba.jit(signature=None, nopython=False, nogil=False, cache=False, forceobj=False, parallel=False, error_model='python', fastmath=False, locals={}, boundscheck=False)
jit装饰器中所有参数都是可选的,如果仅使用@jit,则由系统自动确定如何进行优化
signature参数(数据类型控制)
首先,我们看一个简单的例子
from numba import jit
@jit
def f(x, y):
# A somewhat trivial example
return x + y
此时,使用f(10,3)和f(1j,2)都可以运行。但是,倘若需要对输入参数和输出参数的数据类型进行控制呢?
from numba import jit, int32, float32, double
@jit(double(float32, int32))
def f(x, y):
# A somewhat trivial example
return x + y
其中,double(float32, int32)就是函数签名,double控制输出参数的数据类型,float32和int32分别控制x和y的数据类型。输出参数的签名可以缺省,有库自动判断。
显然,此时再使用f(1j,2)便会报错,实现了数据类型的控制。
常用的数据签名:
- void表示返回值为空
- intp和uintp表示pointer-sized整数(分别表示有符号和无符号)
- intel和uintc相当于C中的int和unsigned int
- int8 uint8, int16, uint16, int32, uint32, int64, uint64是相应的固定宽度的整数位宽度
- float32 float64单、双精度浮点数
- complex64和complex128是单精度和双精度的复数
- 数组类型,如float32[:]和int8[:,:]
nopython、forceobj参数(编译模式选择)
两个参数都是布尔类型,nopython为True表示编译时使用nopython模式,而forceobj为True表示使用object模式。
nopython模式:生成不访问Python C API的代码的Numba编译模式。这种编译模式生成了性能最高的代码,但要求能够推断出函数中所有值的本机类型。除非另有指示,否则如果不能使用nopython模式,@jit装饰器将自动退回到对象模式。
object模式:一种Numba编译模式,它生成将所有值作为Python对象处理的代码,并使用Python C API对这些对象执行所有操作。在对象模式下编译的代码通常不会比Python解释的代码运行得快,除非Numba编译器能够利用循环j。
一般情况下,建议使用nopython模式,毕竟我们使用Numba的目的就是提高运行速度,但在编码规范上有相应限制。
nogil参数(全局进程锁限制)
若nogil为True表示释放全局进程锁,从而可以有效利用多核系统,但只能在nopython模式下使用。另外,使用时需要注意多线程编程的常见陷阱(一致性、同步、竞争条件等)。
cache参数(保存为文件缓存)
若cache为True,则缓存启用基于文件的缓存,以便在前一次调用中已编译函数时缩短编译时间。
parallel参数(并行化参数)
若parallel为True,那么可以自动地并行化许多常见的Numpy构造,并融合相邻的并行操作,从而最大化缓存的局部性。
error_model参数
‘python’or’numpy’,决定以哪个库为准抛出异常
fastmath参数
fastmath支持使用LLVM文档中描述的不安全的浮点转换。此外,如果Intel SVML安装得更快,但是使用了一些数学内部特性的不太精确的版本。
locals参数
指定特定局部变量的类型
boundscheck参数
是否进行数组边界的索引检查,建议不做即设置为True,避免影响速度
@generated_jit()装饰器
@numba.generated_jit(nopython=False, nogil=False, cache=False, forceobj=False, locals={})
generated_jit()装饰器可以根据传入参数的类型决定函数不同的实现方式,同时能够保证jit()装饰器的速度
# 返回给定值是否是缺失类型
import numpy as np
from numba import generated_jit, types
@generated_jit(nopython=True)
def is_missing(x):
"""
Return True if the value is missing, False otherwise.
"""
if isinstance(x, types.Float):
return lambda x: np.isnan(x)
elif isinstance(x, (types.NPDatetime, types.NPTimedelta)):
# The corresponding Not-a-Time value
missing = x('NaT')
return lambda x: x == missing
else:
return lambda x: False
@vectorize()装饰器
@numba.vectorize(*, signatures=[], identity=None, nopython=True, target='cpu', forceobj=False, cache=False, locals={})
编译修饰函数,并将其包装为Numpy ufunc或Numba DUFunc
@jitclass()装饰器
jitclass()对将类中的函数都使用nopython模式进行编译
import numpy as np
from numba import jitclass # import the decorator
from numba import int32, float32 # import the types
spec = [
('value', int32), # a simple scalar field
('array', float32[:]), # an array field
]
@jitclass(spec)
class Bag(object):
def __init__(self, value):
self.value = value
self.array = np.zeros(value, dtype=np.float32)
@property
def size(self):
return self.array.size
def increment(self, val):
for i in range(self.size):
self.array[i] = val
return self.array
@cfunc()装饰器
cfunc()创建一个可以使用外部的C语言代码进行调用的编译后的程序,从而可以与用C或C++编写的库进行交互。考虑到很多Python库的底层是C或C++,该功能十分有用。
例如,scipy.integrate.quad函数即可以接受普通的Python回调,也可以接受包装在ctypes回调对象中的C回调。
使用普通的Python回调
import numpy as np
import scipy.integrate as si
def integrand(t):
return np.exp(-t) / t ** 2
def do_integrate(func):
"""
Integrate the given function from 1.0 to +inf.
"""
return si.quad(func, 1, np.inf)
do_integrate(integrand)
使用cfunc()装饰器
import numpy as np
import scipy.integrate as si
from numba import cfunc
def integrand(t):
return np.exp(-t) / t ** 2
def do_integrate(func):
"""
Integrate the given function from 1.0 to +inf.
"""
return si.quad(func, 1, np.inf)
nb_integrand = cfunc("float64(float64)")(integrand)
do_integrate(nb_integrand.ctypes)
编写规范
一部分报错源于对数据类型不匹配,根据报错调整即可;另一部分报错可能源于numba不主持某些函数,具体可以查阅文档,支持的python特性的链接,支持的numpy特性的链接
性能对比的例子:SVD算法
以推荐系统中的SVD算法为例,展示numba库对速度的提升。
import numpy as np
import time
import pandas as pd
from numba import jit, prange
@jit(nopython=True, cache=True, nogil=True, parallel=True)
def svd(users, items, iterations, lr, reg, factors, avg, data):
# initialization
bu = np.random.normal(loc=0, scale=0.1, size=(users, 1))
bi = np.random.normal(loc=0, scale=0.1, size=(items, 1))
p = np.random.normal(loc=0, scale=0.1, size=(users, factors))
q = np.random.normal(loc=0, scale=0.1, size=(items, factors))
# iteration
for iteration in prange(iterations):
# error use: for u, i, r in trainset:
for line in prange(data.shape[0]):
u, i, r = data[line]
rp = avg + bu[u] + bi[i] + np.dot(q[i], p[u])
e_ui = r - rp
bu[u] += lr * (e_ui - reg * bu[u])
bi[i] += lr * (e_ui - reg * bi[i])
p[u] += lr * (e_ui * q[i] - reg * p[u])
q[i] += lr * (e_ui * p[u] - reg * q[i])
nUsers = 100 # number of users
nItems = 100 # number of items
iteration = 30 # number of iterations
lr = 0.01 # learning rate
reg = 0.002 # regularization rate
factor = 5 # number of factors
trainset = pd.read_csv("D:/py3/trainset.txt", sep=' ', header=None).values
aver = np.mean(trainset[:, 2]) # average rating
start = time.clock()
svd(nUsers, nItems, iteration, lr, reg, factor, aver, trainset)
end = time.clock()
print("training time: %s seconds" % (end - start))
倘若去掉@jit(nopython=True, cache=True, nogil=True),即不使用numba加速的结果为
training time: 17.564660734381764 seconds
倘若使用numba加速,第一次运行时因为需要编译
training time: 9.588356848133849 seconds
之后,再次运行,用时就可以稳定在
training time: 0.18296860820512134 seconds
numba在速度上提升了96倍,一般来说numba可以提升一到两个数量级。