什么?Python太慢?试试Numba库吧!

官方文档

官方文档入口
有需要的小伙伴请点入享用

Python编译过程和执行原理

本节参考了传送门1传送门2
C/C++之类的编译性语言编写的程序,将源文件转换成计算机使用的机器语言,经过链接器链接之后形成了二进制的可执行文件。运行该程序的时候,就可以把二进制程序从硬盘载入到内存中并运行。而在Python作为解释型语言,没有编译这一步,而是由解释器将源代码转换为字节码,然后再由解释器来执行这些字节码,因此Python中不用担心程序的编译、库的链接加载等问题。

Python解释器(如CPython、IPython、PyPy、Jython、IronPython)执行Python代码的四个过程

  1. 词法分析
    检查关键字等是否正确
  2. 语法分析
    语句格式是否正确
  3. 生成字节码
    生成.pyc文件(PyCodeObject对象)。在编译代码的过程中,首先会将代码中的函数、类等对象分类处理,然后生成字节码文件。
  4. 执行
    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可以提升一到两个数量级。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值