python|python性能优化全面指南


文章目录

    • 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编写第三饭模块
    我们将详细的对每一种技术做一个介绍。
创建一门新的语言,这门语言能够写起来像python,跑起来像c++
创建一门新的语言绝非易事,需要考虑到很多状况,世界上的编程语言不下百种,但是能够一般程序员有所耳闻的可能不到十种;但是已经有一些语言按照这种思路被开发了出来。
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 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.
经过Nuitka优化过的程序可以加速312%倍,希望如此!nuitka和 cython一样,可以生成python模块,也可以直接生成可执行程序。使用nuitka的另一个功能就是将python程序打包成独立的二进制文件,方便在其他同类型的电脑系统上运行,这有点类似py2installer,不过比py2installer好的是,它对程序做出了一些优化,使程序运行得更快一些。
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.
It is meant to efficiently compile scientific programs, and takes advantage of multi-cores and SIMD instruction units.
pythran据称是比较好的处理了numpy的调用。pythran是只能生成python模块,而且需要对python的代码进行一定的注释,否则是不能直接生成模块的。
## 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是许多其他科学计算库的基础库。
  • 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.
测试pypy我们不需要改变python代码。
## 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扩展模块,而不必手动去设置编译参数等头疼的事情。

    推荐阅读