Python與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使用擴充套件即可。