文章目录
-
- python、c++与文言文、白话文
- 鱼和熊掌兼而得之
-
- 创建一门新的语言,这门语言能够写起来像python,跑起来像c++
-
- Julia
- Nim
- 拼命提升高级语言Python的运行效率
-
- 将python转化成c、c++代码进行优化
-
- cython
- nuitka
- pythran
- 11l
- 使用JIT技术提高python效率
-
- pypy
- numba
- 用c++编写python的扩展模块
- pybind11
- 其他常用的bind c++为python扩展模块的工具
- 总结
-
- pyfaster
python、c++与文言文、白话文 python语言简单易用,写起代码来就像用我们平常的话来描述流程图,平易近人的不得了,做个类比来说,python就像是现在的白话文,它很容易学会理解,而c++像是文言文。同样一件事情,用白话文洋洋洒洒一千字,还觉得意犹未尽,而用文言文,不到一百
字就已经完全表达了我们的意思。这让我想起来民国时候关于白话文和文言文的争论,有个小故事,那时候发电报按照字的数量来收费,一字一块大洋,一般人不遇到急事是不会破费的。有固守文言文的人以此为论据,说是用白话文,发电报,多么的浪费钱!推崇白话文反驳说,有的事情并不好用文言文来描述,比如,朋友要给你发一封急电,说“老婆不幸死了,请马上回来处理后事”,用文言文不见得能说得这么明白吧!另一人立即说,这简单,只需要发“妻死速归”。前后一对比,省了十几块大洋啊!当然,文言文的言简意赅,就像霍夫编码,熵很高,同样一段文本,可以蕴含更多的信息。用文言文和白话文的难易程度和信息量,可以类比python和c++的难易程度与执行效率。
语言 | 难易程度 | 效率 |
---|---|---|
文言文 | 难 | 高 |
白话文 | 易 | 低 |
python | 易 | 低 |
c++ | 难 | 高 |
- 创建一门新的语言,结合python和c++各自的有点
- 利用JIT技术对python进行性能优化
- 用c、c++给Python编写第三饭模块
我们将详细的对每一种技术做一个介绍。
创建一门新的语言绝非易事,需要考虑到很多状况,世界上的编程语言不下百种,但是能够一般程序员有所耳闻的可能不到十种;但是已经有一些语言按照这种思路被开发了出来。
Julia 这是最值得推荐的一门新语言,被开发出来也就三四年吧,现在已经颇有名声。它的编程方式,结合了python、matlab的风格,比较简单易学;同时它可以声明变量的类型,拥有JIT技术,所谓的JIT就是just-in-time的缩写,是在运行时对程序进行编译,而c++等是AOT方式,即ahead-of-time,是在运行之前提前编译。这些特性使得Julia的执行效率堪比c/c++。这个语言被设计为主要用于科学计算领域,已经拥有很多像python pypi那样的第三方库。
# julia 代码片段
function dprod(l0, l1)
return l0 * l1
end
l0 = transpose(Float64[i for i in 0:99999])
l1 = Float64[i for i in 0:99999]
@time dprod(l0, l1)
代码输出
0.000249 seconds
3.33283335e11
需要
0.249 ms
的时间。上面这个代码片段,从语法上看,很像Python和Matlab的结合,需要用缩进,但是没有冒号,多了很多的end。如果是矩阵的加减乘除,也和MATLAB一样,多一个
.
是逐个元素的。Julia是可以调用Python的任何库的,也比较容易用c++写模块。Nim 或许值得一试,它的语法也算比较自然,但是和python差距比Julia要大一些,具有十分强大的宏功能。
# Nim 代码片段
import strformattype
Person = object
name: string
age: Natural # Ensures the age is positivelet people = [
Person(name: "John", age: 45),
Person(name: "Kate", age: 30)
]for person in people:
# Type-safe string interpolation,
# evaluated at compile time.
echo(fmt"{person.name} is {person.age} years old")
拼命提升高级语言Python的运行效率
这种方式也已经有很多人去做,针对python而言,将python程序进行加速有很多方式。我们这里讨论的不是用c++给python写第三方模块,我将会在后续文章中详细介绍一下如何使用pybind11将c++程序转变为Python的第三方模块。
这里讨论的是,程序是用python写的,如何来加速这段程序的执行。总而言之,也有两种思路。
将python转化成c、c++代码进行优化 这就像是将“老婆死了,快回来处理后事”改为言简意赅的“妻死速归”,没有深厚的文言功底自然是做不了的。现在比较值得一试的类似软件有以下几个:
cython cython已经非常成熟了,它允许你用python的语法来写c代码;同时,它也有自己多出来的一些语法,主要是用于变量的类型声明等。cython的原理是将你写的Python(或者说cython)代码,先翻译为c、c++语言,再编译为Python模块。对于任何的Python代码,你可以不经过任何改动使用cython生成模块,它的运行速度往往会超过原先的python代码,加速两倍左右;如果稍微改一下,注明变量类型等,并去除掉一些参数的合法检查,它的效率几乎逼近c代码。可以参考我之前写过的一篇介绍cython、pypy、numba的文章:python性能优化的比较:numba,pypy, cython。cython可以生成python模块,也可以嵌入python解释器,直接生成可执行程序。
pip install cython
## dprod.pydef dprod(l0, l1):
n = len(l0)
r = 0
for i in range(n):
r += l0[i] * l1[i]
return r
运行如下命令生成模块
cython --cplus -p -3 -a dprod.py -o ./cython_ext/dprod.cpp
这个命令会生成两个文件
./cython_ext/dprod.cpp
./cython_ext/dprod.html
.cpp
文件是翻译的c++文件,.html
文件是一些提示,逐行告诉你python代码中每一句都被翻译成了什么c++代码,可以根据这个来进一步优化修改python代码。生成python模块
g++ `python3-config --cflags --ldflags` -O3 --shared -fPIC -o ./cython_ext/dprod`python3-config --extension-suffix` ./cython_ext/dprod.cpp
这个将会生成可以导入的python模块
./cython_ext/dprod.cpython-37m-x86_64-linux-gnu.so
性能测试结果:
pure python version, average time: 6.188211954000053 ms
cython version, average time: 1.8879676910000853 ms
以上是对python程序未做任何的修改,编译出来的模块运行速度会有两三倍左右的提高,如何稍微用cython的语法来修改一下代码,速度会成倍的提高。
## dprod.pyxcython程序的后缀是pyxfrom cython cimport boundscheck, wraparound@boundscheck(False)
@wraparound(False)
def dprod(double[:] l0, double[:] l1):
cdef:
long n, i
double r
n = l0.shape[0]
r = 0
for i in range(n):
r += l0[i] * l1[i]
return r
使用和上面相同的命令生成python模块,如果用
timeit
测试一下性能from cython_ext.dprod import dprod as dprod_cythonx = list(range(100000))
y = list(range(100000))
xarr = np.asarray(x, dtype=int)
yarr = np.asarray(y, dtype=int)
num = 1000duration = timeit.timeit("dprod_cython(xarr, yarr)",
globals=globals(), number=num)
print(f"cython version, average time: {duration/num * 1000} ms")
结果
cython version, average time: 0.13119223199964836 ms
第一个版本的大概需要
1.8 ms
,而第二个版本的只需要惊人的0.13ms
,作为参考,numpy
需要0.046 ms
,纯python的版本需要6.2ms
。也就是cython
优化后的程序,效率可以是python程序的将近100倍,即使不做任何修改,性能也提高一倍。使用cython
语法写的程序,性能几乎可以媲美numpy
这种专业做矩阵运算的库。scipy
大量的程序就使用了cython
是不无道理的。nuitka 这个也是将Python翻译为c、c++进行优化,生成python模块。
Nuitka is a Python compiler written in Python.
It’s fully compatible with Python2 (2.6, 2.7) and Python3 (3.3 … 3.8).
You feed it your Python app, it does a lot of clever things, and spits out an executable or extension module.
Free license (Apache).
Right now Nuitka is a good replacement for the Python interpreter and compiles every construct that all relevant CPython versions, and even irrelevant ones, like 2.6 and 3.3 offer. It translates the Python into a C program that then is linked against经过Nuitka优化过的程序可以加速312%倍,希望如此!nuitka和 cython一样,可以生成python模块,也可以直接生成可执行程序。使用nuitka的另一个功能就是将python程序打包成独立的二进制文件,方便在其他同类型的电脑系统上运行,这有点类似py2installer,不过比py2installer好的是,它对程序做出了一些优化,使程序运行得更快一些。libpython
to execute in the same way as CPython does, in a very compatible way.
It is somewhat faster than CPython already, but currently it doesn’t make all the optimizations possible, but a 312% factor on pystone is a good start (number is from version 0.6.0 and Python 2.7), and many times this is not generally achieved yet.
pip install nuitka
## dprod.pydef dprod(l0, l1):
n = len(l0)
r = 0
for i in range(n):
r += l0[i] * l1[i]
return r
如果需要生成模块,则添加
--module
选项,如果要生成可执行文件,去掉该选项即可。python3 -m nuitka --module --output-dir ./nuitka_ext dprod.py
这将生成以下python模块:
./nuitka_ext/dprod.so
测试结果
nuitka version, average time: 4.430712274000143 ms
nuitka 生成的模块进行性能测试,结果大概在4.43 ms左右,只比纯python的版本稍微好一点。
pythran pythran 和上面的类似,将python翻译为c、c++然后编译成模块,供python调用。
Pythran is an ahead of time compiler for a subset of the Python language, with a focus on scientific computing. It takes a Python module annotated with a few interface description and turns it into a native Python module with the same interface, but (hopefully) faster.pythran据称是比较好的处理了numpy的调用。pythran是只能生成python模块,而且需要对python的代码进行一定的注释,否则是不能直接生成模块的。
It is meant to efficiently compile scientific programs, and takes advantage of multi-cores and SIMD instruction units.
## dprod.py
## 注意函数上面的注释,没有它,pythran是不能工作的# pythran export dprod(float64[:], float[:])
def dprod(l0, l1):
n = len(l0)
r = 0
for i in range(n):
r += l0[i] * l1[i]
return r
pythran -w -o ./prthran_ext/dprod`python3-config --extension-suffix` dprod.py
这将会生成一个python模块:
./prthran_ext/dprod.cpython-37m-x86_64-linux-gnu.so
测试结果
pythran version, average time: 0.3394267869998657 ms
pythran生成模块的性能测试大概在0.34 ms左右,鉴于它对源码修改比cython要少,所以还是挺不错。
11l 这是一个有着奇怪名字的软件,它实际上是一个中间语言,它将python翻译为11l语言,然后再把11l语言翻译为c++。它最大的好处是翻译出来的c++程序,比其他几种都更适合人来阅读。cythan、pythran、nuitka等的翻译,是包含了大量的python wrapper代码的,读起来很困难。而11l没有,正因为如此,它只能将翻译后的结果编译为可执行文件,而不能是python的扩展模块。
我们对上面的dprod.py做一些typing,可以是翻译出来的程序更舒服一点,不然会有很多的auto关键字。
from typing import Listdef dprod(l0: List[float], l1: List[float]) -> float:
n: int = len(l0)
r: float = 0
for i in range(n):
r += l0[i] * l1[i]
return r
翻译后的c++文件:
#include "11l/_11l_to_cpp/11l.hpp"double dprod(Array &l0, Array &l1)
{
int n = l0.len();
double r = 0;
for (auto i : range_el(0, n))
r += l0[i] * l1[i];
return r;
}int main()
{
}
头文件
11l.hpp
定义了一些常用的类型和函数,这里的Array继承了std::vector。这个翻译可以说很直观,如果我们需要用c++重写一遍已经用python实现的代码,那么借助这个工具,可以省事很多。使用JIT技术提高python效率 除了上面讲的把python翻译为c++语言来提高效率,还有另外一种思路就是给python加上JIT技术。这个在julia里面已经提到了,使用JIT,即时的将反复执行的代码编译为机器码,来提高执行效率。对于python而言,有两个项目专注与这里,分别是pypy和numba。
pypy pypy 是一个用python语言实现的python解释器,具有JIT的功能。它能够兼容绝大部分的python代码,但是对于numpy的支持,似乎总有点问题,而numpy是许多其他科学计算库的基础库。
测试pypy我们不需要改变python代码。
- Speed: thanks to its Just-in-Time compiler, Python programs often run faster on PyPy. (What is a JIT compiler?)
- Memory usage: memory-hungry Python programs (several hundreds of MBs or more) might end up taking less space than they do in CPython.
- Compatibility: PyPy is highly compatible with existing python code. It supports cffi, cppyy, and can run popular python libraries like twisted and django.
- Stackless: PyPy comes by default with support for stackless mode, providing micro-threads for massive concurrency.
## dprod.pydef dprod(l0, l1):
n = len(l0)
r = 0
for i in range(n):
r += l0[i] * l1[i]
return rif __name__ == "__main__":
import timeit
x = [float(i) for i in range(100000)]
y = [float(i) for i in range(100000)]
num = 1000duration = timeit.timeit("dprod(x, y)", globals=globals(), number=num)
print(f"pypy version, average time: {duration/num * 1000} ms")
sudo apt-get install pypy3
pypy3 test/dprod.py>>pypy version, average time: 0.24620251699980142 ms
大概需要
0.246 ms
,这是一个很不错的结果,什么代码也没有改变,相比于python加速了30多倍。numba numba 可以对python程序中的一段代码进行JIT,往往是一个函数,只需要在函数上加一个装饰器就可以,它对numpy的支持十分友好。稍微修改一个我们的代码:
## dprod.pydef dprod(l0, l1):
n = len(l0)
r = 0
for i in range(n):
r += l0[i] * l1[i]
return r##############################################
# for numba test
from numba import njit@njit
def dprod1(l0, l1):
n = l0.shape[0]
r = 0
for i in range(n):
r += l0[i] * l1[i]
return rif __name__ == "__main__":
import timeit
import numpy as np
x = [float(i) for i in range(100000)]
y = [float(i) for i in range(100000)]
xarr = np.asarray(x)
yarr = np.asarray(y)
num = 1000dprod1(xarr, yarr)
duration = timeit.timeit("dprod1(xarr, yarr)",
globals=globals(), number=num)
print(f"numba version, average time: {duration/num * 1000} ms")duration = timeit.timeit("np.dot(xarr, yarr)",
globals=globals(), number=num)
print(f"numpy version, average time: {duration/num * 1000} ms")
python3 test/dprod.py
numba version, average time: 0.1277403009999034 ms
numpy version, average time: 0.02811251999992237 ms
这段程序的测试结果达到了
0.1277 ms
,相比于python版本,速度提高了快百倍。而对代码的修改也并不多,十分值得推荐,但是主要用于数值计算,如果把上面的参数类型改为python的list,速度会严重下降。用c++编写python的扩展模块
pybind11
优化python的效率,另外一个最常用的方法就是用c++来编写python的扩展模块。一般会针对代码中计算密集型、效率要求高的部分,用c++重写实现一遍,而python代码更多关注于逻辑。
将已有的c++代码编译为python模块,需要写一些wrap代码来实现两者之间的数据转换,现在有有一些工具可以十分方便的辅助我们实现这个目标。pybind11是众多工具中,相当简单、轻量、高效的一个。除了pybind11,上面提到的cython也可以,不过会稍微繁琐一点。此外针对c语言的代码,使用cffi来写扩展,也比较方便。
下面是一个pybind11的例子:
// test/dprod.cpp#include #include namespace py = pybind11;
using namespace py::literals;
double dprod(const double *l0, const double *l1, unsigned n)
{
double r = 0;
for (unsigned i = 0;
i < n;
++i)
{
r += l0[i] * l1[i];
}
return r;
}PYBIND11_MODULE(dprod, m)
{
m.def("dprod", [](py::array_t l0, py::array_t l1) {
py::buffer_info l0buf = l0.request(), l1buf = l1.request();
return dprod((double *)l0buf.ptr, (double *)l1buf.ptr, l0buf.shape[0]);
});
}
将上面的代码编译为python模块:
g++ `python3-config --cflags --ldflags` -I`python3 -c "import pybind11;
print(pybind11.get_include())"` -O3 --shared -fPIC -o test/pybind11_ext/dprod`python3-config --extension-suffix` test/dprod.cpp
这段代码的性能测试大概在
0.12101731899997503 ms
,有百倍的效率提高,而我们可以测试一下c++的原始执行速度:#include
#include
#include
#include double dprod(const double *l0, const double *l1, unsigned n)
{
double r = 0;
for (unsigned i = 0;
i < n;
++i)
{
r += l0[i] * l1[i];
}
return r;
}int main()
{
std::vector l0(100000), l1(100000);
std::iota(l0.begin(), l0.end(), 0);
std::iota(l1.begin(), l1.end(), 0);
auto t0 = std::chrono::high_resolution_clock::now();
for (unsigned i = 0;
i < 1000;
++i)
{
double r = dprod(l0.data(), l1.data(), l0.size());
}
auto t1 = std::chrono::high_resolution_clock::now();
std::chrono::duration duration = t1 - t0;
std::cout << "average time: " << duration.count()/ 1000 << " ms\n";
return 0;
}
编译开启-O3选项,运行时间是
2.907e-06 ms
,速度太快了,而将它wrap成python模块,运行时间是0.12 ms
,这中间相差的时间,都被数据转换给浪费了。其他常用的bind c++为python扩展模块的工具
除了pybind11,还有几个比较常用,这里仅仅列出:
- swig,不仅仅python和c++
- cffi,主要面向c语言写的代码
- cppyy,可以在python里指定c++模板参数
- boost.python,pybind11 脱胎于此
- cppimport ,实际上是让pybind11更加方便一点
numpy version, average time: 0.02811251999992237 ms
pybind11 version, average time: 0.12712017100056983 ms
numba version, average time: 0.1277403009999034 ms
cython version, average time: 0.12953058299990516 ms
pypy version, average time: 0.24620251699980142 ms
pythran version, average time: 0.33915015000002313 ms
nuitka version, average time: 4.488496339999983 ms
pure python version, average time: 6.111591079999926 ms
优化后的python在数值计算方面效率可以提高百倍。
pyfaster
【python|python性能优化全面指南】以上的测试例子代码,可以在pyfaster找到,同时它可以很方便的让你生成python扩展模块,而不必手动去设置编译参数等头疼的事情。
推荐阅读
- python|Python爬虫之mongodb介绍和安装
- python|深入浅出——Mybaties 入门码农避坑必备(一)
- ArcGIS|Linux上安装ArcGIS for Server超详细教程——以Redhat6.5上安装ArcGIS for Server 10.3.1为例
- Python|Python ImportError libGL.so.1 cannot open shared object file No such file or directory 解决方案
- Linux 学习 15
- python|python实现opencv学习十七(hough变换检测直线)
- 使用 Python3 脚本给多个人同时发送多个 excel 附件
- Pytorch_python|神经网络的基本组成之卷积层(Conv Layer)&& 利用 Pytorch 搭建神经网络 && 空洞卷积对比普通卷积
- 用Python绘制移动均线含源代码