python 一直被病垢运行速度太慢, 但是实际上 python 的执行效率并不慢, 慢的是 python 用的解释器 Cpython 运行效率太差."一行代码让 python 的运行速度提高 100 倍" 这绝不是哗众取宠的论调.
Python 用的好, 猪也能飞起来.
今天, 带大家学习如何让 Python 飞起来的方法, 干货满满哦!
python 一直被病垢运行速度太慢, 但是实际上 python 的执行效率并不慢, 慢的是 python 用的解释器 Cpython 运行效率太差.
"一行代码让 python 的运行速度提高 100 倍" 这绝不是哗众取宠的论调.
我们来看一下这个最简单的例子, 从 1 一直累加到 1 亿.
最原始的代码:
- import time
- def foo(x,y):
- tt = time.time()
- s = 0
- for i in range(x,y):
- s += i
- print('Time used: {} sec'.format(time.time()-tt))
- return s
- print(foo(1,100000000))
结果:
- Time used: 6.779874801635742 sec
- 4999999950000000
我们来加一行代码, 再看看结果:
- from numba import jit
- import time
- @jit
- def foo(x,y):
- tt = time.time()
- s = 0
- for i in range(x,y):
- s += i
- print('Time used: {} sec'.format(time.time()-tt))
- return s
- print(foo(1,100000000))
结果:
- Time used: 0.04680037498474121 sec
- 4999999950000000
是不是快了 100 多倍呢?
那么下面就分享一下 "为啥 numba 库的 jit 模块那么牛掰?"
NumPy 的创始人 Travis Oliphant 在离开 Enthought 之后, 创建了 CONTINUUM, 致力于将 Python 大数据处理方面的应用. 最近推出的 Numba 项目能够将处理 NumPy 数组的 Python 函数 JIT 编译为机器码执行, 从而上百倍的提高程序的运算速度.
Numba 项目的主页上有 Linux 下的详细安装步骤. 编译 LLVM 需要花一些时间.
Windows 用户可以从 Unofficial Windows Binaries for Python Extension Packages 下载安装 LLVMPy,meta 和 numba 等几个扩展库.
下面我们看一个例子:
- import numba as nb
- from numba import jit
- @jit('f8(f8[:])')
- def sum1d(array):
- s = 0.0
- n = array.shape[0]
- for i in range(n):
- s += array[i]
- return s
- import numpy as np
- array = np.random.random(10000)
- %timeit sum1d(array)
- %timeit np.sum(array)
- %timeit sum(array)
- 10000 loops, best of 3: 38.9 us per loop
- 10000 loops, best of 3: 32.3 us per loop
- 100 loops, best of 3: 12.4 ms per loop
numba 中提供了一些修饰器, 它们可以将其修饰的函数 JIT 编译成机器码函数, 并返回一个可在 Python 中调用机器码的包装对象. 为了能将 Python 函数编译成能高速执行的机器码, 我们需要告诉 JIT 编译器函数的各个参数和返回值的类型. 我们可以通过多种方式指定类型信息, 在上面的例子中, 类型信息由一个字符串'f8(f8[:])'指定. 其中'f8'表示 8 个字节双精度浮点数, 括号前面的'f8'表示返回值类型, 括号里的表示参数类型,'[:]'表示一维数组. 因此整个类型字符串表示 sum1d()是一个参数为双精度浮点数的一维数组, 返回值是一个双精度浮点数.
需要注意的是, JIT 所产生的函数只能对指定的类型的参数进行运算:
- print sum1d(np.ones(10, dtype=np.int32))
- print sum1d(np.ones(10, dtype=np.float32))
- print sum1d(np.ones(10, dtype=np.float64))
- 1.2095376009e-312
- 1.46201599944e+185
- 10.0
如果希望 JIT 能针对所有类型的参数进行运算, 可以使用 autojit:
- from numba import autojit
- @autojit
- def sum1d2(array):
- s = 0.0
- n = array.shape[0]
- for i in range(n):
- s += array[i]
- return s
- %timeit sum1d2(array)
- print sum1d2(np.ones(10, dtype=np.int32))
- print sum1d2(np.ones(10, dtype=np.float32))
- print sum1d2(np.ones(10, dtype=np.float64))
- 10000 loops, best of 3: 143 us per loop
- 10.0
- 10.0
- 10.0
autoit 虽然可以根据参数类型动态地产生机器码函数, 但是由于它需要每次检查参数类型, 因此计算速度也有所降低. numba 的用法很简单, 基本上就是用 jit 和 autojit 这两个修饰器, 和一些类型对象. 下面的程序列出 numba 所支持的所有类型:
- print [obj for obj in nb.__dict__.values() if isinstance(obj, nb.minivect.minitypes.Type)]
- [size_t, Py_uintptr_t, uint16, complex128, float, complex256, void, int , long double,
- unsigned PY_LONG_LONG, uint32, complex256, complex64, object_, npy_intp, const char *,
- double, unsigned short, float, object_, float, uint64, uint32, uint8, complex128, uint16,
- int, int , uint8, complex64, int8, uint64, double, long double, int32, double, long double,
- char, long, unsigned char, PY_LONG_LONG, int64, int16, unsigned long, int8, int16, int32,
- unsigned int, short, int64, Py_ssize_t]
工作原理
numba 的通过 meta 模块解析 Python 函数的 ast 语法树, 对各个变量添加相应的类型信息. 然后调用 llvmpy 生成机器码, 最后再生成机器码的 Python 调用接口.
meta 模块
通过研究 numba 的工作原理, 我们可以找到许多有用的工具. 例如 meta 模块可在程序源码, ast 语法树以及 Python 二进制码之间进行相互转换. 下面看一个例子:
- def add2(a, b):
- return a + b
decompile_func 能将函数的代码对象反编译成 ast 语法树, 而 str_ast 能直观地显示 ast 语法树, 使用这两个工具学习 Python 的 ast 语法树是很有帮助的.
- from meta.decompiler import decompile_func
- from meta.asttools import str_ast
- print str_ast(decompile_func(add2))
- FunctionDef(args=arguments(args=[Name(ctx=Param(),
- id='a'),
- Name(ctx=Param(),
- id='b')],
- defaults=[],
- kwarg=None,
- vararg=None),
- body=[Return(value=BinOp(left=Name(ctx=Load(),
- id='a'),
- op=Add(),
- right=Name(ctx=Load(),
- id='b')))],
- decorator_list=[],
- name='add2')
而 python_source 可以将 ast 语法树转换为 Python 源代码:
- from meta.asttools import python_source
- python_source(decompile_func(add2))
- def add2(a, b):
- return (a + b)
decompile_pyc 将上述二者结合起来, 它能将 Python 编译之后的 pyc 或者 pyo 文件反编译成源代码. 下面我们先写一个 tmp.py 文件, 然后通过 py_compile 将其编译成 tmp.pyc.
- with open("tmp.py", "w") as f:
- f.write("""
- def square_sum(n):
- s = 0
- for i in range(n):
- s += i**2
- return s
- """)
- import py_compile
- py_compile.compile("tmp.py")
下面调用 decompile_pyc 将 tmp.pyc 显示为源代码:
- with open("tmp.pyc", "rb") as f:
- decompile_pyc(f)
- def square_sum(n):
- s = 0
- for i in range(n):
- s += (i ** 2)
- return s
llvmpy 模块
LLVM 是一个动态编译器, llvmpy 则可以通过 Python 调用 LLVM 动态地创建机器码. 直接通过 llvmpy 创建机器码是比较繁琐的, 例如下面的程序创建一个计算两个整数之和的函数, 并调用它计算结果.
- from llvm.core import Module,
- Type,
- Builder from llvm.ee import ExecutionEngine,
- GenericValue#Create a new module with a
- function implementing this: ##int add(int a, int b) {#
- return a + b;#
- }#my_module = Module.new('my_module') ty_int = Type.int() ty_func = Type.
- function(ty_int, [ty_int, ty_int]) f_add = my_module.add_function(ty_func, "add") f_add.args[0].name = "a"f_add.args[1].name = "b"bb = f_add.append_basic_block("entry")#IRBuilder
- for our basic block builder = Builder.new(bb) tmp = builder.add(f_add.args[0], f_add.args[1], "tmp") builder.ret(tmp)#Create an execution engine object.This will create a JIT compiler#on platforms that support it,
- or an interpreter otherwise ee = ExecutionEngine.new(my_module)#Each argument needs to be passed as a GenericValue object,
- which is a kind#of variant arg1 = GenericValue.int(ty_int, 100) arg2 = GenericValue.int(ty_int, 42)#Now let 's compile and run!
- retval = ee.run_function(f_add, [arg1, arg2])
- # The return value is also GenericValue. Let's print it.print "returned",
- retval.as_int() returned 142
f_add 就是一个动态生成的机器码函数, 我们可以把它想象成 C 语言编译之后的函数. 在上面的程序中, 我们通过 ee.run_function 调用此函数, 而实际上我们还可以获得它的地址, 然后通过 Python 的 ctypes 模块调用它.
首先通过 ee.get_pointer_to_function 获得 f_add 函数的地址:
- addr = ee.get_pointer_to_function(f_add)
- addr
- 2975997968L
然后通过 ctypes.PYFUNCTYPE 创建一个函数类型:
- import ctypes
- f_type = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)
最后通过 f_type 将函数的地址转换为可调用的 Python 函数, 并调用它:
- f = f_type(addr)
- f(100, 42)
- 142
numba 所完成的工作就是:
解析 Python 函数的 ast 语法树并加以改造, 添加类型信息;
将带类型信息的 ast 语法树通过 llvmpy 动态地转换为机器码函数, 然后再通过和 ctypes 类似的技术为机器码函数创建包装函数供 Python 调用.
来源: http://developer.51cto.com/art/201809/583695.htm