如何加速Python的运行速度

一、Python为什么执行慢?

Python 之所以速度慢,主要是由于动态性和多功能性。具体的关于Python执行速度的解释可以见这篇博客。究竟有多慢?以Fibonacci数列的计算为例,分别使用C++和Python执行相同功能的代码,比较其执行时间

  • 使用C++
#include <iostream>
#include <time.h>

using namespace std;

int fibonacci(int n){
	if(n == 1 || n == 2){return 1;}
	else{
		return fibonacci(n - 2) + fibonacci(n - 1);
	}

}
int main(){
	clock_t startTime,endTime;
	int res = fibonacci(40);
	endTime = clock();
	cout << (double)(endTime-startTime) / CLOCKS_PER_SEC << endl;    
	
	return 0;
}

在我的mac上面执行时间为0.4s,我们再来看看使用Python3的时间。

  • 使用python
import time
def fibonacci(n):
    if n == 1 or n ==2:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)

if __name__ == '__main__':
	start = time.time()
	fibonacci(40)
	end = time.time()
	print(end - start)

同样的硬件,执行时间为24s。可以看到python相比C++执行速度慢了足足有60倍!

二、提高python执行速度的几种方法

2.1 使用Numba

Numba是Python的即时编译器,它最适用于使用NumPy数组和函数以及循环的代码。使用Numba的最常用方法是通过其装饰器集合,可以应用于你的函数来指示Numba编译它们。当调用Numba修饰函数时,它被编译为机器代码“及时”执行,并且你的全部或部分代码随后可以以本机机器代码速度运行!

2.1.1 Numba适用的场景

一般的,适用于Numba加速的程序一般具备下面的特征:

  • 代码以数字为导向
  • 经常适用Numpy
  • 循环多

2.1.2 Numba执行过程

下面看一个简单的例子,使用的是Numba的jit装饰器。

import time
from numba import jit
import numpy as np

@jit(nopython=True)
def go_fast(a):
	trace = 0
	for i in range(a.shape[0]): 
		for j in range(a.shape[1]):
			trace += a[i, j] * a[i, j]
	return a + trace           

if __name__ == '__main__':
	x = np.arange(10000).reshape(100, 100)
	start = time.time()
	go_fast(x)
	end = time.time()
	print(end - start)  # 0.3720729351043701

	start = time.time()
	go_fast(x)
	end = time.time()
	print(end - start)  # 1.0967254638671875e-05

这段代码执行函数为Numpy数组的循环计算。适用于Numba加速的场景,我们可以看到第一次执行函数的时候耗时0.37s,第二次执行耗时仅有0.00001s,差距巨大,第一次函数执行时间甚至比直接调用函数更慢(直接调用函数执行时间约为0.004s),为什么会出现这种差异呢?

Numba必须为执行函数的机器代码版本之前给出的参数类型编译函数,这需要时间。但是,一旦编译完成,Numba会为所呈现的特定类型的参数缓存函数的机器代码版本。如果再次使用相同的类型调用它,它可以重用缓存的版本而不必再次编译。这就是为什么上述函数前后两次执行时间差距巨大的原因。

2.1.3 @git装饰器的编译模式

Numba @jit装饰器从根本上以两种编译模式运行, nopython模式和object模式。在go_fast上面的例子中, nopython=True在@jit装饰器中设置,这是指示Numba在nopython模式下操作。nopython编译模式的行为本质上是编译装饰函数,以便它完全运行而不需要Python解释器的参与。这是使用Numba jit装饰器的推荐和最佳实践方式,因为它可以带来最佳性能。

下面看一下@git不能理解的类型,我们运行一下观察函数执行的时间。

import time
from numba import jit
import pandas as pd

@jit()
def use_pandas(a):
    df = pd.DataFrame.from_dict(a)
    df += 1 
    return df.cov()       

if __name__ == '__main__':
	x = {'a': [1, 2, 3], 'b': [20, 30, 40]}
	start = time.time()
	use_pandas(x)
	end = time.time()
	print(end - start)     # 0.7431411743164062

	start = time.time()
	use_pandas(x)
	end = time.time()
	print(end - start)     # 0.002971172332763672

这个例子在运行的时候会出现编译警告⚠️(如果使用nopython模式,会报错)。虽然第二次执行时间更短,但是如果直接不使用@git,会发现执行时间为0.018s,提升的幅度相比于2.1.2中的例子要小的多。

解释:如果编译nopython模式失败,Numba可以编译使用 ,如果没有设置,这是装饰器的后退模式(如上例所示)。在这种模式下,Numba将识别它可以编译的循环并将它们编译成在机器代码中运行的函数,并且它将运行解释器中的其余代码。为获得最佳性能,请避免使用此模式。

最后,我们再次使用文章一开始提到的Fibonacci数列的计算测试Numba的执行效果,程序如下:

import time
from numba import jit
import time

@jit(nopython=True)
def fibonacci(n):
    if n == 1 or n ==2:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)

if __name__ == '__main__':
	start = time.time()
	fibonacci(40)
	end = time.time()
	print(end - start)   # 0.8565220832824707

运行时间为0.8s,虽说比不上C++的性能,但是相比于直接使用python还是快了几十倍!

2.2 使用Cython

Cython 是Python语言的一个超集,对其你可以为Python写C 或C++模块。Cython也使得你可以从已编译的C库中调用函数。使用Cython让你得以发挥Python 的变量与操作的强类型优势。

Cython 的本质可以总结如下:Cython 是包含 C 数据类型的 Python。

另外,Cython 程序需要先编译之后才能被 Python 调用,流程是:

  1. Cython 编译器把 Cython 代码编译成调用了 Python 源码的 C/C++ 代码
  2. 把生成的代码编译成动态链接库
  3. Python 解释器载入动态链接库

Cython程序在编写的时候一般可以分为三种:

  1. 纯Python函数
  2. 带有静态类型声明的函数
  3. 使用C函数

2.2.1 使用纯Python函数

下面我们就来写一个最简单的Cython程序,注意程序命名为test.pyx,后缀是.pyx不是.py

# test.pyx
def fibonacci(n):
    if n == 1 or n ==2:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)

首先需要完成Cython的编译,我们要写如下代码:

# setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy
setup(ext_modules = cythonize(Extension(
    'test',
    sources=['test.pyx'],
    language='c',
    include_dirs=[numpy.get_include()],
    library_dirs=[],
    libraries=[],
    extra_compile_args=[],
    extra_link_args=[]
)))

然后在终端对上述python代码进行编译,指令如下

python setup.py build_ext --inplace

成功运行完上面这句话,可以看到在当前目录多出来了 test.c 和 test.so。前者是生成的 C 程序,后者是编译好了的动态链接库。

接着来看一下测试效果:

import time
import test
import time

if __name__ == '__main__':
	start = time.time()
	test.fibonacci(40)
	end = time.time()
	print(end - start)   # 4.635458946228027

可以看到程序运行时间相比直接使用Python速度快了大约5倍。

2.2.2 使用静态类型声明

我们在上一个test.pyx的基础上加上一个静态类型声明,即

def fibonacci(int n):
    if n == 1 or n ==2:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)

看看测试效果

import time
import test
import time

if __name__ == '__main__':
	start = time.time()
	test.fibonacci(40)
	end = time.time()
	print(end - start)   # 4.2128190994262695

可以看到执行时间进一步的缩短,接下来我们使用C函数看看效果。

2.2.3 使用C函数

同样的,在test.pyx的基础上做下面的修改

cdef helper(int n):
    if n == 1 or n ==2:
        return 1
    return helper(n-1) + helper(n-2)


def fibonacci(int n):
    return helper(n)

看看执行效果

import time
import kd_tree
import time

if __name__ == '__main__':
	start = time.time()
	kd_tree.fibonacci(40)
	end = time.time()
	print(end - start)   # 1.4132821559906006

可以看到,这次运行速度再一次提升,相比于直接调用Python程序效率提升了约17倍。

关于Cython的东西还很多,我这里也只是起一个抛砖引玉的作用,想了解更多可以参考Cython教程

2.3 调用C程序

python调用C函数的过程比较简单,同样的也是需要将.c文件编译成.so的动态链接库,编写C程序如下:

//testc.c
int fibonacci(int n){
	if(n == 1 || n == 2){return 1;}
	else{
		return fibonacci(n - 2) + fibonacci(n - 1);
	}
}

使用下面的指令,将其编译为.so的动态链接库

gcc -o testc.so -shared -fPIC testc.c

然后在python代码中,我们需要调用ctypes模块加载动态链接库

import time
import ctypes  
import time

if __name__ == '__main__':
	start = time.time()
	ll = ctypes.cdll.LoadLibrary   
	lib = ll("./testc.so")  
	lib.fibonacci(40)
	end = time.time()
	print(end - start)   # 0.4253208637237549

可以看到执行速度为0.4s,和直接使用C++代码的执行速度相差无几。

参考

[1] https://blog.csdn.net/sinat_38682860/article/details/81457078
[2] https://www.cnblogs.com/yhleng/p/9920666.html
[3] https://zhuanlan.zhihu.com/p/24311879
[4] https://www.cnblogs.com/yanzi-meng/p/8066944.html

  • 8
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值