当前位置: 首页 > 科技观察

一行代码让你的Python运行速度提升100倍!Python真的很强!

时间:2023-03-22 12:35:40 科技观察

Python用得好,猪都能飞。今天,我就带大家学习如何让Python飞起来。满满的干货!Python一直运行的太慢,但其实python的执行效率并不慢。慢的是python使用的解释器Cpython的运行效率。太糟糕了。“一行代码让python运行速度快100倍”这绝不是哗众取宠的说法。我们来看一个最简单的例子,从1到1亿累加。最原始的代码:importtimedeffoo(x,y):tt=time.time()s=0foriinrange(x,y):s+=iprint('Timeused:{}sec'.format(time.time()-tt))returnsprint(foo(1,100000000))Result:Timeused:6.779874801635742sec4999999950000000我们加一行代码看看结果:fromnumbaimportjitimporttime@jitdeffoo(x,y):tt=time.time()s=0foriinrange(x,y):s+=iprint('Timeused:{}sec'.format(time.time()-tt))returnsprint(foo(1,100000000))结果:Timeused:0.04680037498474121sec4999999950000000是不是快了100多倍?那我给大家分享一下“numba库的jit模块为什么这么牛逼?”NumPy创始人TravisOliphant离开Enthought后创建了CONTINUUM,致力于Python大数据处理的应用。最近上线的Numba项目可以将处理NumPy数组的Python函数JIT编译为机器码执行,从而将程序的运算速度提升数百倍。Numba项目主页有详细的Linux下安装步骤。编译LLVM需要一些时间。Windows用户可以从UnofficialWindowsBinariesforPythonExtensionPackages下载并安装几个扩展库,例如LLVMPy、meta和numba。让我们看下面的例子:importnumbaasnbfromnumbaimportjit@jit('f8(f8[:])')defsum1d(array):s=0.0n=array.shape[0]foriinrange(n):s+=array[i]returnsimportnumpyasnparray=np.random.random(10000)%timeitsum1d(array)%timeitnp.sum(array)%timeitsum(array)10000loops,bestof3:38.9usperloop10000loops,bestof3:32.3usperloop100loops,bestof3:12.4msperloopnumba提供了一些装饰器,它们可以被装饰function被JIT编译成机器代码函数,并返回一个可以在Python中调用机器代码的包装对象。为了将Python函数编译成可以高速执行的机器码,我们需要告诉JIT编译器函数的每个参数和返回值的类型。我们可以通过多种方式来指定类型信息,在上面的例子中,类型信息是由一个字符串'f8(f8[:])'来指定的。其中'f8'代表一个8字节的双精度浮点数,括号前的'f8'代表返回值类型,括号内的代表参数类型,'[:]'表示一维数组。因此,整个类型字符串表明sum1d()是一个一维数组,参数为双精度浮点数,返回值为双精度浮点数。需要注意的是,JIT生成的函数只能对指定类型的参数进行操作:printsum1d(np.ones(10,dtype=np.int32))printsum1d(np.ones(10,dtype=np.float32))printsum1d(np.ones(10,dtype=np.float64))1.2095376009e-3121.46201599944e+18510.0如果想让JIT对所有类型的参数都进行操作,可以使用autojit:fromnumbaimportautojit@autojitdefsum1d2(array):s=0.0n=array.shape[0]foriinrange(n):s+=array[i]returns%timeitsum1d2(array)printsum1d2(np.ones(10,dtype=np.int32))printsum1d2(np.ones(10,dtype=np.float32))printsum1d2(np.ones(10,dtype=np.float64))10000loops,bestof3:143usperloop10.010.010.0autoit可以根据参数类型动态生成机器码函数,但是由于需要每次检查参数类型时间,因此计算速度也降低了。numba的用法很简单,基本就是用jit和autojit这两个修饰符,以及一些类型对象。以下程序列出了numba支持的所有类型:,unsignedPY_LONG_LONG,uint32,complex256,complex64,object_,npy_intp,constchar*,double,unsignedshort,float,object_,float,uint64,uint32,uint8,complex128,uint16,int,int,uint8,complex64,int8,uint64,double,longdouble,int32,double,longdouble,char,long,unsignedchar,PY_LONG_LONG,int64,int16,unsignedlong,int8,int16,int32,unsignedint,short,int64,Py_ssize_t]工作原理numba通过meta模块解析Python函数的ast语法树,为每个变量添加对应的类型信息。然后调用llvmpy生成机器码,最后生成机器码的Python调用接口。元模块通过研究numba的工作原理,我们可以找到很多有用的工具。例如,meta模块可以在程序源代码、ast语法树和Python二进制代码之间进行转换。我们看一个例子:defadd2(a,b):returna+bdecompile_func可以将函数的代码对象反编译成ast语法树,str_ast可以直观的展示ast语法树。使用这两个工具学习Python的ast语法树是很有帮助的。frommeta.decompileimportdecompile_funcfrommeta.asttoolsimportstr_astprintstr_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源码:frommeta.asttoolsimportpython_sourcepython_source(decompile_func(add2))defadd2(a,b):return(a+b)decompile_pyc结合了以上两者,可以将Python编译后的pyc或pyo文件反编译成源代码。下面我们先写一个tmp.py文件,然后通过py_compile编译成tmp.pyc。withopen("tmp.py","w")asf:f.write("""defsquare_sum(n):s=0foriinrange(n):s+=i**2returns""")importpy_compilepy_compile.compile("tmp.py")下面调用decompile_pyc以将tmp.pyc显示为源代码:withopen("tmp.pyc","rb")asf:decompile_pyc(f)defsquare_sum(n):s=0foriinrange(n):s+=(i**2)返回llvmpy模块LLVM是一个动态编译器,llvmpy可以通过Python调用LLVM动态创建机器码。直接通过llvmpy创建机器码比较麻烦。例如,下面的程序创建了一个计算两个整数之和的函数,并调用它来计算结果。fromllvm.coreimportModule,Type,Builderfromllvm.eeimportExecutionEngine,GenericValue#Createanewmodulewithafunctionimplementingthis:##intadd(inta,intb){#returna+b;#}#my_module=Module.new('my_module')ty_int=类型.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")#IRBuilderforourbasicblockbuilder=Builder.new(bb)tmp=builder.add(f_add.args[0],f_add.args[1],"tmp")builder.ret(tmp)#创建一个执行引擎对象。这将创建一个JIT编译器#onplatformsthatsupportingit,oraninterpreterotherwiseee=ExecutionEngine.new(my_module)#EachargumentneedstobepassedasGenericValueobject,whichisakind#ofvariantarg1=GenericValue.int(ty_int,100)arg2=GenericValue.int(ty_int,42)#Nowlet'scompileandrun!retval=ee_add,_function(f[arg1,arg2])#returnvalueisalsoGenericValue.Let'sprintit.print"returned",retval.as_int()返回的142f_add是一个动态生成的机器码函数,我们可以把它看成是C语言编译出来的函数在上面的程序中,我们通过ee.run_function调用这个函数,但实际上我们也可以得到它的地址,然后通过Python的ctypes模块调用它。先通过ee.get_pointer_to_function获取f_add函数的地址:addr=ee.get_pointer_to_function(f_add)addr2975997968L然后通过ctypes.PYFUNCTYPE创建函数类型:importctypesf_type=ctypes.PYFUNCTYPE(ctypes.c_int,ctypes.c_int,ctypes.c_int)***通过f_type把函数的地址转换成可调用的Python函数,调用:f=f_type(addr)f(100,42)142numba做的是:解析Python函数的ast语法树,修改它,添加类型信息;通过llvmpy将带有类型信息的ast语法树动态转换为机器码函数,然后通过类似ctypes的技术为Python调用创建机器码函数的包装函数。