文章目录
一、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 调用,流程是:
- Cython 编译器把 Cython 代码编译成调用了 Python 源码的 C/C++ 代码
- 把生成的代码编译成动态链接库
- Python 解释器载入动态链接库
Cython程序在编写的时候一般可以分为三种:
- 纯Python函数
- 带有静态类型声明的函数
- 使用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