python+C、C++混合程式設計的應用 薦
TIOBE每個月都會新鮮出爐一份流行程式語言排行榜,這裡會列出最流行的20種語言。排序說明不了語言的好壞,反應的不過是某個軟體開發領域的熱門程度。語言的發展不是越來越common,而是越來越專注領域。有的語言專注於簡單高效,比如python,內建的list,dict結構比c/c++易用太多,但同樣為了安全、易用,語言也犧牲了部分效能。在有些領域,比如通訊,效能很關鍵,但並不意味這個領域的coder只能苦苦掙扎於c/c++的陷阱中,比如可以使用多種語言混合程式設計。 我看到的一個很好的Python與c/c++混合程式設計的應用是NS3(Network Simulator3)一款網路模擬軟體,它的內部計算引擎需要用高效能,但在使用者建模部分需要靈活易用。NS3的選擇是使用C/C++來模擬核心部件和協議,用python來建模和擴充套件。 這篇文章介紹python和c/c++三種混合程式設計的方法,並對效能加以分析。
混合程式設計的原理
首先要說一下python只是一個語言規範,實際上python有很多實現:CPython是標準Python,是由C編寫的,python指令碼被編譯成CPython位元組碼,然後由虛擬機器解釋執行,垃圾回收使用引用計數,我們談與C/C++混合程式設計實際指的是基於CPython解釋上的。除此之外,還有Jython、IronPython、PyPy、Pyston,Jython是Java編寫的,使用JVM的垃圾回收,可以與Java混合程式設計,IronPython面向.NET平臺。 python與C/C++混合程式設計的本質是python呼叫C/C++編譯的動態連結庫,關鍵就是把python中的資料型別轉換成c/c++中的資料型別,給編譯函式處理,然後返回引數再轉換成python中的資料型別。
python中使用ctypes moduel,將python型別轉成c/c++型別
首先,編寫一段累加數值的c程式碼:
extern "C" { int addBuf(char* data, int num, char* outData); } int addBuf(char* data, int num, char* outData) { for (int i = 0; i < num; ++i) { outData[i] = data[i] + 3; } return num; }
然後,將上面的程式碼編譯成so庫,使用下面的編譯指令
>gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC addbuf.c -o addbuf.o
最後編寫python程式碼,使用ctypes庫,將python型別轉換成c語言需要的型別,然後傳參呼叫so庫函式:
from ctypes import * # cdll, c_int lib = cdll.LoadLibrary('libmathBuf.so') callAddBuf = lib.addBuf num = 4 numbytes = c_int(num) data_in = (c_byte * num)() for i in range(num): data_in[i] = i data_out = (c_byte * num)() ret = lib.addBuf(data_in, numbytes, data_out)#呼叫so庫中的函式
在C/C++程式中使用Python.h,寫wrap包裝介面
這種方法需要修改c/c++程式碼,在外部函式中處理入/出參,適配python的引數。寫一段c程式碼將外部入參作為shell命令執行:
#include <Python.h> static PyObject* SpamError; static PyObject* spam_system(PyObject* self, PyObject* args) { const char* command; int sts; if (!PyArg_ParseTuple(args, "s", &command))//將args引數按照string型別處理,給command賦值 return NULL; sts = system(command); //呼叫系統命令 if (sts < 0) { PyErr_SetString(SpamError, "System command failed"); return NULL; } return PyLong_FromLong(sts);//將返回結果轉換為PyObject型別 } //方法表 static PyMethodDef SpamMethods[] = { {"system", spam_system, METH_VARARGS, "Execute a shell command."}, {NULL, NULL, 0, NULL} }; //模組初始化函式 PyMODINIT_FUNC initspam(void) { PyObject* m; //m = PyModule_Create(&spammodule); // v3.4 m = Py_InitModule("spam", SpamMethods); if (m == NULL) return; SpamError = PyErr_NewException("spam.error",NULL,NULL); Py_INCREF(SpamError); PyModule_AddObject(m,"error",SpamError); }
處理上所有的入參、出參都作為PyObject物件來處理,然後使用轉換函式把python的資料型別轉換成c/c++中的型別,返回引數按相同方式處理。比第一種方法多了初始化函式,這部分是把編譯的so庫當做python module所必需要做的。 python這樣使用:
imoprt spam spam.system("ls")
使用c/c++編寫python擴充套件可以參見:http://docs.python.org/2.7/extending/extending.html
使用SWIG,來生成獨立的wrap檔案
這種方式並不能算是一種新方式,實際上是基於第二中方式的一種包裝。SWIG是個幫助使用C或者C++編寫的軟體能與其它各種高階程式語言進行嵌入聯接的開發工具。SWIG能應用於各種不同型別的語言包括常用指令碼編譯語言例如Perl, PHP, Python, Tcl, Ruby, PHP,C#,Java,R等。 操作上,是針對c/c++程式編寫獨立的介面宣告檔案(通常很簡單),swig會分析c/c++源程式自動分析介面要如何包裝。在指定目標語言後,swig會生成額外的包裝原始碼檔案。編譯so庫時,把包裝檔案一起編譯、連線即可。看個c程式碼例子:
int system(const char* command) { sts = system(command); if (sts < 0) { return NULL; } return sts; }
c原始碼中去掉適配python的包裝,僅定義system函式本身,這比第二種方式簡潔很多,並且剔除了c程式碼與python的耦合程式碼,是c程式碼通用性更好。
然後編寫swig介面宣告檔案spam.i:
%module spam %{ #include "spam.h" %} %include "spam.h" %include "typemaps.i" int system(const char* INPUT);
這是一段語言無關的模組宣告,要建立一個叫spam的模組,對system做一個宣告,主要是宣告引數作為入參使用。然後執行swig編譯程式:
>swig -c++ -python spam.i
swig會生成spam_wrap.cxx和spam.py兩個檔案。先看spam_wrap.cxx,這個生成的檔案很長,但關鍵的就是對函式的包裝:

包裝函式傳入的還是PyObejct物件,內部進行了型別轉換,最終調了原始碼中的system函式。
生成的了另一個spam.py實際上是對so庫又用python包裝了一層(實際比較多餘):

這裡使用_spam模組,這裡實際上是把擴充套件命名為了_spam。關於swig在python上的應用可以參見:http://www.swig.org/Doc1.3/Python.html 下面就是編譯和安裝python 模組,Python提供了distutils module,可以很方便的編譯安裝python的module。像下面這樣寫一個安裝指令碼setup.py:  執行python setup.py build,即可以完成編譯,程式會建立一個build目錄,下面有編譯好的so庫。so庫放在當前目錄下,其實Python就可以通過import來載入模組了。當然也可以用 python setup.py install 把模組安裝到語言的擴充套件庫——site-packages目錄中。關於build python擴充套件,可以參考https://docs.python.org/2/extending/building.html#building
混合程式設計效能分析
混合程式設計的使用場景中,很重要一個就是效能攸關。那麼這小節將通過幾個小實驗驗證下混合程式設計的效能如何,或者說怎樣寫程式能發揮好混合程式設計的效能優勢。
我們使用氣泡排序演算法來驗證效能。
1)實驗一 使用冒泡程式驗證python和c/c++程式的效能差距
python版冒泡程式:
def bubble(arr,length): j = length - 1 while j >= 0: i = 0 while i < j: if arr[i] > arr[i+1]: tmp = arr[i+1] arr[i+1] = arr[i] arr[i] = tmp i += 1 j -= 1
c語言版氣泡排序
void bubble(int* arr,int length){ int j = length - 1; int i; int tmp; while(j >= 0){ i = 0; while(i < j){ if(arr[i] > arr[i+1]){ tmp = arr[i+1]; arr[i+1] = arr[i]; arr[i] = tmp; } i += 1; } j -= 1; } }
使用一個長度為100內容固定的陣列,反覆排序10000次(每次排序後,再把陣列恢復成原始序列),記錄執行時間: 在相同的機器上多次執行,Python版執行時間是10.3s左右,而c語言版本(未使用任何優化編譯引數)執行時間只有0.29s左右。相比之下python的效能的確差很多(主要是python中list的操作跟c的陣列相比,效率差非常多),但python中很多擴充套件都是c語言寫的,目的就是為了提升效率,python用於資料分析的numpy庫就擁有不錯的效能。下個實驗就驗證,如果python使用c語言版本的氣泡排序擴充套件庫,效能會提升多少。
2)實驗二 python語言使用ctypes方式呼叫
這裡直接使用c_int來定義了陣列物件,這也節省了呼叫時資料型別轉換的開銷:
import time from ctypes import * IntArray100 = c_int * 100 arr = IntArray100(87,23,41, 3, 2, 9,10,23,0,21,5,15,93, 6,19,24,18,56,11,80,34, 5,98,33,11,25,99,44,33,78, 52,31,77, 5,22,47,87,67,46,83, 89,72,34,69, 4,67,97,83,23,47, 69, 8, 9,90,20,58,20,13,61,99,7,22,55,11,30,56,87,29,92,67, 99,16,14,51,66,88,24,31,23,42,76,37,82,10, 8, 9, 2,17,84,32,66,77,32,17, 5,68,86,22, 1, 0) ... ... if __name__ == "__main__": libbubble = CDLL('libbubble.so') time1 = time.time() for i in xrange(100000): libbubble.initArr(arr1,arr,100) libbubble.bubble(arr1,100) time2 = time.time() print time2 - time1
再次執行:
為了減少誤差,把迴圈增加到10萬次,結果c原生程式使用優化引數編譯後用時0.65s左右。python使用c擴充套件後(相同編譯引數)執行僅需2.3s左右。
3)實驗三 在c語言中使用PyObject處理入參
這種方式是在python中依然使用list裝入待排序數列,在c函式中把list賦值給陣列,再進行排序,排好序後,再對原始list賦值。迴圈排序10萬次,執行用時1.0s左右。
4) 實驗四 使用swig來包裝c方法
在介面檔案中宣告%array_class(int,intArray);然後在Python中使用initArray來作為陣列,同樣修改成10萬次排序。python版本的程式(相同編譯引數)執行僅需0.7s左右,比c原生程式慢大概7%。
結論
1.python 的list效率非常低,在高效能場景下避免對list大量迴圈、取值、賦值操作。如需要最好使用ctype中的陣列,或者是用c語言來實現。 2.應該把耗時的cpu密集型的邏輯交給c/c++實現,python使用擴充套件即可。