Numba是个为特定对象的开源JIT编译器, 它由公司带头开展开发工作, 能够针对原生代码实施CPU以及GPU加速, Numba对于NumPy数组跟函数特别友善。
Numba简介
计算机能够执行的仅仅是二进制的机器码, C、C++这类编译型语言得依靠编译器把源代码转变为可执行文件之后才能够运行, Java等解释型语言借助解释器对源代码进行翻译后于虚拟机上开展执行。对于解释型语言而言, 因为解释器的存在, 致使其执行效率比起C语言要慢上几倍甚至几十倍。

几十年发展历程的C语言, 其优化已然达至极致状态。以C语言作为基准而言, 不少的解释语言, 诸如R之类, 会慢上十倍之多甚至达到一百倍的减缓程度。Julia这一解释语言属于特殊存在, 原因在于它运用了JIT编译技术。
解释器工作原理
这是一门用于作出解释的语言, 它借由硬件以及操作系统给我们构建起一个虚拟机, 并且运用解释器把源代码转变成虚拟机能够执行的字节码。字节码在虚拟机上面进行执行。最后获得执行结果。

Just-In-Time(JIT)
Just-In-Time(JIT)技术针对解释语言给出了一种优化方式, 这种优化能够克服上述提及的效率问题, 能够极大程度地提升代码执行的速度, 并且还能保留语言本身所具有的易用性。在使用JIT技术期间, JIT编译器会将源代码编译成机器能够直接去执行的机器语言, 而且可以直接在中央处理器等硬件上运行。如此一来就跳过了原本的虚拟机, 其执行速度几乎和使用C语言进行编程的速度基本上没有什么差别。
Numba
Numba是一个针对的开源JIT编译器,由公司主导开发,可以对原生代码进行CPU和GPU加速。Numba对NumPy数组和函数非常友好。
简介
借助Numba极为便利, 只需于原生函数之上增添一个具有特定功能的符号组合(), Numba会把这些函数运用即时编译为JIT的方式转化成能被机器识别执行的代码, 这些经转化的代码会以近乎机器执行其自身代码时的速度来运行。
目前,Numba对以下环境进行了支持:
安装方法
使用conda安装Numba:
javascript
`$ conda install numba`
或者使用pip安装:
javascript
`$ pip install numba`
使用方法
使用时,只需要在原来的函数上添加一行"注释":
javascript
`from numba import jit
import numpy as np
SIZE = 2000
x = np.random.random((SIZE, SIZE))
"""
给定n*n矩阵,对矩阵每个元素计算tanh值,然后求和。
因为要循环矩阵中的每个元素,计算复杂度为 n*n。
"""
@jit
def jit_tan_sum(a): # 函数在被调用时编译成机器语言
tan_sum = 0
for i in range(SIZE): # Numba 支持循环
for j in range(SIZE):
tan_sum += np.tanh(a[i, j]) # Numba 支持绝大多数NumPy函数
return tan_sum
print(jit_tan_sum(x))`
我们仅仅只需于原本的代码之上增添一行@jit, 便能把一个函数编译成机器码, 其他的地方均无需进行更改, @符号对原来的代码予以了装饰, 故而将类似这般的写法称作装饰器。
使用场景
Numba, 简单得很, 仅仅是在函数上面加上一个装饰,就能够让程序加速, 然而, 它也是存在缺点的。当前的情况是, Numba仅仅支持了原生函数以及部分NumPy函数, 在别的一些场景当中, 它可能就不适用。
Numba 工作模式
实际上, 通常建议把代码里计算密集的那部分内容, 当作单独的函数提取出来, 并且采用方式进行优化, 这般能够确保我们可以运用到Numba的加速功效。其余的部分依旧运用原生代码, 在实现计算加速的状况下, 防止产生过长的编译时间。Numba能够与NumPy紧密地结合在一起, 二者相互配合, 往往能够获取近乎C语言的速度。虽说Numba没办法直接进行优化, 不过我们能够把其中处理数据的for循环, 作为单独的函数提取出来, 然后再借助Numba来加速。
编译开销
进行编译源代码是需要一定时间的, C/C++等编译型语言是要预先把整个程序先予以编译好, 之后再去执行可执行文件。Numba库所提供的是一种属于懒编译(Lazy)的技术, 也就是在运行进程里第一次发觉代码当中存在@jit, 才会将该代码块进行编译。是在用到的时候才开展编译, 看上去较为懒, 故而称作懒编译。
运用Numba之际, 总体时间等于编译时间加上运行时间。相较于能够节省的计算时间而言, 编译所产生的时间耗费是微小的, 故而可谓物有所值。针对一个需要进行多次调用的Numba函数, 仅需编译一回, 后续再度调用之时便无需编译了。
javascript
`from numba import jit
import numpy as np
import time
SIZE = 2000
x = np.random.random((SIZE, SIZE))
"""
给定n*n矩阵,对矩阵每个元素计算tanh值,然后求和。
因为要循环矩阵中的每个元素,计算复杂度为 n*n。
"""
@jit
def jit_tan_sum(a): # 函数在被调用时编译成机器语言
tan_sum = 0
for i in range(SIZE): # Numba 支持循环
for j in range(SIZE):
tan_sum += np.tanh(a[i, j]) # Numba 支持绝大多数NumPy函数
return tan_sum
# 总时间 = 编译时间 + 运行时间
start = time.time()
jit_tan_sum(x)
end = time.time()
print("Elapsed (with compilation) = %s" % (end - start))
# Numba将加速的代码缓存下来
# 总时间 = 运行时间
start = time.time()
jit_tan_sum(x)
end = time.time()
print("Elapsed (after compilation) = %s" % (end - start))`
在代码里, 有两次对那个Numba优化函数进行调用, 第一次执行的时刻, 是需要去编译的, 而第二次呢, 是使用经过缓存的代码, 如此一来, 运行的时间就会被大大地缩短:
javascript
`Elapsed (with compilation) = 0.49199914932250977
Elapsed (after compilation) = 0.0364077091217041`
类型推断编译加速
另一个致使原生速度慢的关键缘由是, 变量类型处于不确定状态。声明一个变量的语法是颇为简易的, 就像a = 1这种情况, 然而却并未明确指出a究竟是一个整数还是一个浮点小数。解释器需要开展大量的类型推断工作, 这将会很耗费时间。在引入Numba之后, Numba与此同时也要对输入输出的类型进行推断, 才能够将其转化为机器码。针对这个问题, 名为Eager的优化方式被Numba给出了。
javascript
`from numba import jit, int32
@jit("int32(int32, int32)", nopython=True)
def f2(x, y):
return x + y`
@jit(int32(int32, int32)), 向Numba说明你的函数运用的是何种输入与输出, 括号里的是输入, 而括号左边的是输出。这般不会使执行速度提升, 不过会加快编译速度, 能够更迅速地把函数编译成机器码。
参考资料