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);
}
/<python.h>

處理上所有的入參、出參都作為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,這個生成的文件很長,但關鍵的就是對函數的包裝:

python+C、C++混合編程的應用

swig會生成spam_wrap.cxx和spam.py兩個文件

包裝函數傳入的還是PyObejct對象,內部進行了類型轉換,最終調了源碼中的system函數。

生成的了另一個spam.py實際上是對so庫又用python包裝了一層(實際比較多餘):

python+C、C++混合編程的應用

python+C、C++混合編程的應用

這裡使用_spam模塊,這裡實際上是把擴展命名為了_spam。關於swig在python上的應用可以參見:http://www.swig.org/Doc1.3/Python.html

下面就是編譯和安裝python 模塊,Python提供了distutils module,可以很方便的編譯安裝python的module。像下面這樣寫一個安裝腳本setup.py:

python+C、C++混合編程的應用

python+C、C++混合編程的應用

執行 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使用擴展即可。


分享到:


相關文章: