轉自:https://zhuanlan.zhihu.com/p/442935082
Python寫起來非常方便, 但面對大量for循環的時候, 執行速度有些捉急. 原因在於, python是一種動態類型語言, 在運行期間才去做數據類型檢查, 這樣效率就很低(尤其是大規模for循環的時候).
相比而言, C/C++每個變量的類型都是事先給定的, 通過編譯生成二進制可執行文件. 相比與python, C/C++效率比較高, 大規模for循環執行速度很快.
既然python的短板在於速度, 所以, 為了給python加速, 能否在Python中調用C/C++的代碼?
當我們編寫Python代碼時,我們得到的是一個包含Python代碼的以.py
為擴展名的文本文件。要運行代碼,就需要Python解釋器去執行.py
文件。
(你給我翻譯翻譯, 什麼叫python代碼)
當我們從Python官方網站下載並安裝好Python後,我們就直接獲得了一個官方版本的解釋器:CPython
。這個解釋器是用C語言開發的,所以叫CPython
。在命令行下運行python
就是啟動CPython
解釋器。CPython
是使用最廣的Python解釋器。
雖然CPython效率低, 但是如果用它去調用C/C++代碼, 效果還是挺好的. 像numpy之類的數學運算庫, 很多都是用C/C++寫的. 這樣既能利用python簡潔的語法, 又能利用C/C++高效的執行速度. 有些情況下numpy效率比自己寫C/C++還高, 因為numpy利用了CPU指令集優化和多核並行計算.
我們今天要講的Python調用C/C++, 都是基於CPython解釋器的.
IronPython
和Jython
類似,只不過IronPython
是運行在微軟.Net
平台上的Python
解釋器,可以直接把Python代碼編譯成.Net的字節碼。缺點在於, 因為numpy
等常用的庫都是用C/C++
編譯的, 所以在IronPython
中調用numpy
等第三方庫非常不方便. (現在微軟已經放棄對IronPython的更新了)
Jython
是運行在Java
平台上的Python
解釋器,可以直接把Python
代碼編譯成Java
字節碼執行。Jython
的好處在於能夠調用Java
相關的庫, 壞處跟IronPython
一樣.
PyPy一個基於Python的解釋器,也就是用python解釋.py. 它的目標是執行速度。PyPy采用JIT技術,對Python代碼進行動態編譯(注意不是解釋),所以可以顯著提高Python代碼的執行速度。
假設我們有一個簡單的python函數
def add(x, y):
return x + y
然後CPython
執行起來大概是這個樣子(偽代碼)
if instance_has_method(x, '__add__') {
// x.__add__ 裡面又有一大堆針對不同類型的 y 的判斷
return call(x, '__add__', y);
} else if isinstance_has_method(super_class(x), '__add__' {
return call(super_class, '__add__', y);
} else if isinstance(x, str) and isinstance(y, str) {
return concat_str(x, y);
} else if isinstance(x, float) and isinstance(y, float) {
return add_float(x, y);
} else if isinstance(x, int) and isinstance(y, int) {
return add_int(x, y);
} else ...
因為Python的動態類型, 一個簡單的函數, 要做很多次類型判斷. 這還沒完,你以為裡面把兩個整數相加的函數,就是 C 語言裡面的 x + y 麼? No.
Python裡萬物皆為對象, 實際上Python裡的int大概是這樣一個結構體(偽代碼).
struct {
prev_gc_obj *obj
next_gc_obj *obj
type IntType
value IntValue
... other fields
}
每個 int 都是這樣的結構體,還是動態分配出來放在 heap 上的,裡面的 value 還不能變,也就是說你算 1000 這個結構體加 1000 這個結構體,需要在heap裡malloc出來 2000 這個結構體. 計算結果用完以後, 還要進行內存回收. (執行這麼多操作, 速度肯定不行)
所以, 如果能夠靜態編譯執行+指定變量的類型, 將大幅提升執行速度.
cython是一種新的編程語言, 它的語法基於python, 但是融入了一些C/C++的語法. 比如說, cython裡可以指定變量類型, 或是使用一些C++裡的stl庫(比如使用std::vector
), 或是調用你自己寫的C/C++函數.
注意: Cython不是CPython!
我們有一個RawPython.py
from math import sqrt
import time
def func(n):
res = 0
for i in range(1, n):
res = res + 1.0 / sqrt(i)
return res
def main():
start = time.time()
res = func(30000000)
print(f"res = {
res}, use time {
time.time() - start:.5}")
if __name__ == '__main__':
main()
我們先使用Python原生方式來執行看一下需要多少時間, 在我電腦上要花4秒。
首先, 把一個cython程序轉化成.c/.cpp
文件, 然後用C/C++
編譯器, 編譯生成二進制文件. 在Windows下, 我們需要安裝Visual Studio/mingw等編譯工具. 在Linux或是Mac下, 我們需要安裝gcc
, clang
等編譯工具.
pip
安裝cythonpip install cython
把 RawPython.py 重命名為 RawPython1.pyx
(1)用setup.py編譯
增加一個setup.py
, 添加以下內容. 這裡language_level的意思是, 使用Python 3.
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize('RawPython1.pyx', language_level=3)
)
把Python編譯為二進制代碼
python setup.py build_ext --inplace
然後, 我們發現當前目錄下多了RawPython1.c(由.pyx
轉化生成), 和RawPython1.pyd
(由.c編譯生成的二進制文件).
(2)直接在命令行編譯(以gcc
為例)
cython RawPython1.pyx
gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python3.x -o RawPython1.so RawPython1.c
第一句是把.pyx轉化成.c, 第二句是用gcc
編譯+鏈接.
python -c "import RawPython1; RawPython1.main()"
我們可以導入編譯好的RawPython1模塊, 然後在Python中調用執行.
由以上的步驟的執行結果來看,並沒有提高太多,只大概提高了一倍的速度,這是因為Python的運行速度慢除了因為是解釋執行以外還有一個最重要的原因是Python是動態類型語言,每個變量在運行前是不知道類型是什麼的,所以即便編譯為二進制代碼同樣速度不會太快,這時候我們需要深度使用Cython
來給Python提速了,就是使用Cython
來指定Python的數據類型。
指定變量類型
cython的好處是, 可以像C語言一樣, 顯式地給變量指定類型. 所以, 我們在cython
的函數中, 加入循環變量的類型.
然後, 用C語言中的sqrt實現開方操作.
def func(int n):
cdef double res = 0
cdef int i, num = n
for i in range(1, num):
res = res + 1.0 / sqrt(i)
return res
但是, python中math.sqrt
方法, 返回值是一個Python
的float
對象, 這樣效率還是比較低.
為了, 我們能否使用C語言的sqrt函數? 當然可以~
Cython
對一些常用的C函數/C++類做了包裝, 可以直接在Cython裡進行調用.
我們把開頭的
from math import sqrt
換成
from libc.math cimport sqrt
再按照上面的方式編譯運行, 發現速度提高了不少.
改造後的完整代碼如下:
import time
from libc.math cimport sqrt
def func(int n):
cdef double res = 0
cdef int i, num = n
for i in range(1, num):
res = res + 1.0 / sqrt(i)
return res
def main():
start = time.time()
res = func(30000000)
print(f"res = {
res}, use time {
time.time() - start:.5}")
if __name__ == '__main__':
main()
Cython調用C/C++
既然C/C++比較高效, 我們能否直接用cython調用C/C++呢? 就是用C語言重寫一遍這個函數, 然後在cython裡進行調用.
首先寫一段對應的C語言版本
usefunc.h
#pragma once
#include <math.h>
double c_func(int n)
{
int i;
double result = 0.0;
for(i=1; i<n; i++)
result = result + sqrt(i);
return result;
}
然後, 我們在Cython
中, 引入這個頭文件, 然後調用這個函數
cdef extern from "usecfunc.h":
cdef double c_func(int n)
import time
def func(int n):
return c_func(n)
def main():
start = time.time()
res = func(30000000)
print(f"res = {
res}, use time {
time.time() - start:.5}")
在Cython中使用numpy
在Cython
中, 我們可以調用numpy
. 但是, 如果直接按照數組下標訪問, 我們還需要動態判斷numpy
數據的類型, 這樣效率就比較低.
import numpy as np
cimport numpy as np
from libc.math cimport sqrt
import time
def func(int n):
cdef np.ndarray arr = np.empty(n, dtype=np.float64)
cdef int i, num = n
for i in range(1, num):
arr[i] = 1.0 / sqrt(i)
return arr
def main():
start = time.time()
res = func(30000000)
print(f"len(res) = {
len(res)}, use time {
time.time() - start:.5}")
解釋:
cimport numpy as np
這一句的意思是, 我們可以使用numpy
的C/C++接口(指定數據類型, 數組維度等).
這一句的意思是, 我們也可以使用numpy
的Python接口(np.array, np.linspace等). Cython
在內部處理這種模糊性,這樣用戶就不需要使用不同的名稱.
在編譯的時候, 我們還需要修改setup.py, 引入numpy
的頭文件.
from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy as np
setup(ext_modules = cythonize(
Extension("RawPython4", ["RawPython4.pyx"],include_dirs=[np.get_include()],),
language_level=3)
)
加速!加速!
上面的代碼, 還是能夠進一步加速的
numpy
數組的數據類型和維度, 這樣就不用動態判斷數據類型了. 實際生成的代碼, 就是按C語言裡按照數組下標來訪問.i
個. 為了做負數下標訪問, 也需要一個額外的if…else…來判斷. 如果我們用不到這個功能, 也可以關掉.最終加速的程序如下:
import numpy as np
cimport numpy as np
from libc.math cimport sqrt
import time
cimport cython
@cython.boundscheck(False) # 關閉數組下標越界
@cython.wraparound(False) # 關閉負索引
@cython.cdivision(True) # 關閉除0檢查
@cython.initializedcheck(False) # 關閉檢查內存視圖是否初始化
def func(int n):
cdef np.ndarray[np.float64_t, ndim=1] arr = np.empty(n, dtype=np.float64)
cdef int i, num = n
for i in range(1, num):
arr[i] = 1.0 / sqrt(i)
return arr
def main():
start = time.time()
res = func(30000000)
print(f"len(res) = {
len(res)}, use time {
time.time() - start:.5}")
cdef np.ndarray[np.float64_t, ndim=1] arr = np.empty(n, dtype=np.float64)
這一句的意思是, 我們創建numpy數組時, 手動指定變量類型和數組維度.
上面是對這一個函數關閉數組下標越界, 負索引, 除0檢查, 內存視圖是否初始化等. 我們也可以在全局范圍內設置, 即在.pyx文件的頭部, 加上注釋
# cython: boundscheck=False
# cython: wraparound=False
# cython: cdivision=True
# cython: initializedcheck=False
也可以用這種寫法:
with cython.cdivision(True):
# do something here
其他
cython吸收了很多C/C++的語法, 也包括指針和引用. 也可以把一個struct/class從C++傳給Cython.
Cython的語法與Python類似, 同時引入了一些C/C++的特性, 比如指定變量類型等. 同時, Cython還可以調用C/C++的函數.
Cython的特點在於, 如果沒有指定變量類型, 執行效率跟Python差不多. 指定好類型後, 執行效率才會比較高.
更多文檔可以參考Cython官方文檔
Welcome to Cython’s Documentationdocs.cython.org/en/latest/index.html
Cython
是一種類Python
的語言, 但是pybind11
是基於C++
的. 我們在.cpp文件中引入pybind11, 定義python程序入口, 然後編譯執行就好了.
從官網的說明中看到pybind11的幾個特點
可以執行pip install pybind11
安裝 pybind11 (萬能的pip)
也可以用Visual Studio + vcpkg+CMake來安裝.
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add_func(int i, int j) {
return i + j;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; //可選,說明這個模塊是做什麼的
m.def("add_func", &add_func, "A function which adds two numbers");
}
首先引入pybind11的頭文件, 然後用PYBIND11_MODULE聲明.
import example
m.def( "給python調用方法名", &實際操作的函數, "函數功能說明" ). //其中函數功能說明為可選
pybind11只有頭文件,所以只要在代碼中增加相應的頭文件, 就可以使用pybind11了.
#include <pybind11/pybind11.h>
在Linux下, 可以執行這樣的命令來編譯:
c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) example.cpp -o example$(python3-config --extension-suffix)
我們也可以用setup.py來編譯(在Windows下, 需要Visual Studio或mingw等編譯工具; 在Linux或是Mac下, 需要gcc或clang等編譯工具)
from setuptools import setup, Extension
import pybind11
functions_module = Extension(
name='example',
sources=['example.cpp'],
include_dirs=[pybind11.get_include()],
)
setup(ext_modules=[functions_module])
然後運行下面的命令, 就可以編譯了
python setup.py build_ext --inplace
在python中進行調用
python -c "import example; print(example.add_func(200, 33))"
通過簡單的代碼修改,就可以通知Python參數名稱
m.def("add", &add, "A function which adds two numbers", py::arg("i"), py::arg("j"));
也可以指定默認參數
int add(int i = 1, int j = 2) {
return i + j;
}
在PYBIND11_MODULE
中指定默認參數
m.def("add", &add, "A function which adds two numbers",py::arg("i") = 1, py::arg("j") = 2);
PYBIND11_MODULE(example, m) {
m.attr("the_answer") = 23333;
py::object world = py::cast("World");
m.attr("what") = world;
}
對於字符串, 需要用py::cast
將其轉化為Python對象.
然後在Python中, 可以訪問the_answer
和what
對象
import example
>>>example.the_answer
42
>>>example.what
'World'
因為python萬物皆為對象, 因此我們可以用py::object
來保存Python中的變量/方法/模塊等.
py::object os = py::module_::import("os");
py::object makedirs = os.attr("makedirs");
makedirs("/tmp/path/to/somewhere");
這就相當於在Python裡執行了
import os
makedirs = os.makedirs
makedirs("/tmp/path/to/somewhere")
我們可以直接傳入python的list
void print_list(py::list my_list) {
for (auto item : my_list)
py::print(item);
}
PYBIND11_MODULE(example, m) {
m.def("print_list", &print_list, "function to print list", py::arg("my_list"));
}
在Python裡跑一下這個程序,
>>>import example
>>>result = example.print_list([2, 23, 233])
2
23
233
>>>print(result)
這個函數也可以用std::vector<int>
作為參數. 為什麼可以這樣做呢? pybind11可以自動將python list對象, 復制構造為std::vector<int>
. 在返回的時候, 又自動地把std::vector
轉化為Python中的list. 代碼如下:
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
std::vector<int> print_list2(std::vector<int> & my_list) {
auto x = std::vector<int>();
for (auto item : my_list){
x.push_back(item + 233);
}
return x;
}
PYBIND11_MODULE(example, m) {
m.def("print_list2", &print_list2, "help message", py::arg("my_list"));
}
因為numpy比較好用, 所以如果能夠把numpy數組作為參數傳給pybind11, 那就非常香了. 代碼如下(一大段)
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
py::buffer_info buf1 = input1.request(), buf2 = input2.request();
if (buf1.ndim != 1 || buf2.ndim != 1)
throw std::runtime_error("Number of dimensions must be one");
if (buf1.size != buf2.size)
throw std::runtime_error("Input shapes must match");
/* No pointer is passed, so NumPy will allocate the buffer */
auto result = py::array_t<double>(buf1.size);
py::buffer_info buf3 = result.request();
double *ptr1 = (double *) buf1.ptr,
*ptr2 = (double *) buf2.ptr,
*ptr3 = (double *) buf3.ptr;
for (size_t idx = 0; idx < buf1.shape[0]; idx++)
ptr3[idx] = ptr1[idx] + ptr2[idx];
return result;
}
m.def("add_arrays", &add_arrays, "Add two NumPy arrays");
先把numpy的指針拿出來, 然後在指針上進行操作.
我們在Python裡測試如下:
>>>import example
>>>import numpy as np
>>>x = np.ones(3)
>>>y = np.ones(3)
>>>z = example.add_arrays(x, y)
>>>print(type(z))
<class 'numpy.ndarray'>
>>>print(z)
array([2., 2., 2.])
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
int add_func(int i, int j) {
return i + j;
}
void print_list(py::list my_list) {
for (auto item : my_list)
py::print(item);
}
std::vector<int> print_list2(std::vector<int> & my_list) {
auto x = std::vector<int>();
for (auto item : my_list){
x.push_back(item + 233);
}
return x;
}
py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
py::buffer_info buf1 = input1.request(), buf2 = input2.request();
if (buf1.ndim != 1 || buf2.ndim != 1)
throw std::runtime_error("Number of dimensions must be one");
if (buf1.size != buf2.size)
throw std::runtime_error("Input shapes must match");
/* No pointer is passed, so NumPy will allocate the buffer */
auto result = py::array_t<double>(buf1.size);
py::buffer_info buf3 = result.request();
double *ptr1 = (double *) buf1.ptr,
*ptr2 = (double *) buf2.ptr,
*ptr3 = (double *) buf3.ptr;
for (size_t idx = 0; idx < buf1.shape[0]; idx++)
ptr3[idx] = ptr1[idx] + ptr2[idx];
return result;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; //可選,說明這個模塊是做什麼的
m.def("add_func", &add_func, "A function which adds two numbers");
m.attr("the_answer") = 23333;
py::object world = py::cast("World");
m.attr("what") = world;
m.def("print_list", &print_list, "function to print list", py::arg("my_list"));
m.def("print_list2", &print_list2, "help message", py::arg("my_list2"));
m.def("add_arrays", &add_arrays, "Add two NumPy arrays");
}
pybind11在C++下使用, 可以為Python程序提供C++接口. 同時, pybind11也支持傳入python list, numpy等對象.
更多文檔可以參考pybind11官方文檔
https://pybind11.readthedocs.io/en/stable/pybind11.readthedocs.io/en/stable/
用Python開發比較簡潔, 用C++開發寫起來有些麻煩.
在寫python時, 我們可以通過Profile等耗時分析工具, 找出比較用時的代碼塊, 對這一塊用C++進行優化. 沒必要優化所有的部分.
Cython或是pybind11只做三件事: 加速, 加速, 還是加速. 在需要大量計算, 比較耗時的地方, 我們可以用C/C++來實現, 這樣有助於提升整個Python程序的執行速度.
加速python還有一些其他的方法, 比如用numpy的向量化操作代替for循環, 使用jit即時編譯等.