TIOBE每個(gè)月都會(huì)新鮮出爐一份流行編程語(yǔ)言排行榜琅轧,這里會(huì)列出最流行的20種語(yǔ)言伍绳。排序說(shuō)明不了語(yǔ)言的好壞,反應(yīng)的不過(guò)是某個(gè)軟件開(kāi)發(fā)領(lǐng)域的熱門程度乍桂。語(yǔ)言的發(fā)展不是越來(lái)越common冲杀,而是越來(lái)越專注領(lǐng)域。有的語(yǔ)言專注于簡(jiǎn)單高效睹酌,比如Python权谁,內(nèi)建的list,dict結(jié)構(gòu)比c/c++易用太多憋沿,但同樣為了安全旺芽、易用,語(yǔ)言也犧牲了部分性能辐啄。在有些領(lǐng)域采章,比如通信,性能很關(guān)鍵壶辜,但并不意味這個(gè)領(lǐng)域的coder只能苦苦掙扎于c/c++的陷阱中悯舟,比如可以使用多種語(yǔ)言混合編程。
我看到的一個(gè)很好的Python與c/c++混合編程的應(yīng)用是NS3(Network Simulator3)一款網(wǎng)絡(luò)模擬軟件砸民,它的內(nèi)部計(jì)算引擎需要用高性能抵怎,但在用戶建模部分需要靈活易用奋救。NS3的選擇是使用C/C++來(lái)模擬核心部件和協(xié)議,用Python來(lái)建模和擴(kuò)展反惕。
這篇文章介紹Python和c/c++三種混合編程的方法尝艘,并對(duì)性能加以分析。
混合編程的原理
首先要說(shuō)一下Python只是一個(gè)語(yǔ)言規(guī)范承璃,實(shí)際上Python有很多實(shí)現(xiàn):CPython是標(biāo)準(zhǔn)Python利耍,是由C編寫的,python腳本被編譯成CPython字節(jié)碼盔粹,然后由虛擬機(jī)解釋執(zhí)行隘梨,垃圾回收使用引用計(jì)數(shù),我們談與C/C++混合編程實(shí)際指的是基于CPython解釋上的舷嗡。除此之外轴猎,還有Jython、IronPython进萄、PyPy捻脖、Pyston,Jython是Java編寫的中鼠,使用JVM的垃圾回收可婶,可以與Java混合編程,IronPython面向.NET平臺(tái)援雇。
Python與C/C++混合編程的本質(zhì)是python調(diào)用C/C++編譯的動(dòng)態(tài)鏈接庫(kù)矛渴,關(guān)鍵就是把Python中的數(shù)據(jù)類型轉(zhuǎn)換成c/c++中的數(shù)據(jù)類型,給編譯函數(shù)處理惫搏,然后返回參數(shù)再轉(zhuǎn)換成Python中的數(shù)據(jù)類型具温。
Python中使用ctypes moduel,將Python類型轉(zhuǎn)成c/c++類型
首先筐赔,編寫一段累加數(shù)值的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庫(kù),使用下面的編譯指令
>gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC addbuf.c -o addbuf.o
最后編寫python代碼茴丰,使用ctypes庫(kù)达皿,將python類型轉(zhuǎn)換成c語(yǔ)言需要的類型,然后傳參調(diào)用so庫(kù)函數(shù):
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) #調(diào)用so庫(kù)中的函數(shù)
在C/C++程序中使用Python.h较沪,寫wrap包裝接口
這種方法需要修改c/c++代碼鳞绕,在外部函數(shù)中處理入/出參,適配python的參數(shù)尸曼。寫一段c代碼將外部入?yún)⒆鳛閟hell命令執(zhí)行:
#include
static PyObject* SpamError;
static PyObject* spam_system(PyObject* self, PyObject* args)
{
const char* command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command)) //將args參數(shù)按照string類型處理们何,給command賦值
return NULL;
sts = system(command); //調(diào)用系統(tǒng)命令
if (sts < 0) {
PyErr_SetString(SpamError, "System command failed");
return NULL;
}
return PyLong_FromLong(sts); //將返回結(jié)果轉(zhuǎn)換為PyObject類型
}
//方法表
static PyMethodDef SpamMethods[] = {
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
{NULL, NULL, 0, NULL}
};
//模塊初始化函數(shù)
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);
}
處理上所有的入?yún)ⅰ⒊鰠⒍甲鳛镻yObject對(duì)象來(lái)處理控轿,然后使用轉(zhuǎn)換函數(shù)把Python的數(shù)據(jù)類型轉(zhuǎn)換成c/c++中的類型冤竹,返回參數(shù)按相同方式處理拂封。比第一種方法多了初始化函數(shù),這部分是把編譯的so庫(kù)當(dāng)做Pythonmodule所必需要做的鹦蠕。
Python這樣使用:
imoprt spam
spam.system("ls")
使用c/c++編寫python擴(kuò)展可以參見(jiàn):http://docs.python.org/2.7/extending/extending.html
使用SWIG冒签,來(lái)生成獨(dú)立的wrap文件
這種方式并不能算是一種新方式,實(shí)際上是基于第二中方式的一種包裝钟病。SWIG是個(gè)幫助使用C或者C++編寫的軟件能與其它各種高級(jí)編程語(yǔ)言進(jìn)行嵌入聯(lián)接的開(kāi)發(fā)工具萧恕。SWIG能應(yīng)用于各種不同類型的語(yǔ)言包括常用腳本編譯語(yǔ)言例如Perl, PHP, Python, Tcl, Ruby, PHP,C#,Java,R等肠阱。
操作上票唆,是針對(duì)c/c++程序編寫?yīng)毩⒌慕涌诼暶魑募ㄍǔ:芎?jiǎn)單),swig會(huì)分析c/c++源程序自動(dòng)分析接口要如何包裝屹徘。在指定目標(biāo)語(yǔ)言后走趋,swig會(huì)生成額外的包裝源碼文件。編譯so庫(kù)時(shí)噪伊,把包裝文件一起編譯簿煌、連接即可〖担看個(gè)c代碼例子:
int system(const char* command)
{
sts = system(command);
if (sts < 0) {
return NULL;
}
return sts;
}
c源碼中去掉適配Python的包裝姨伟,僅定義system函數(shù)本身,這比第二種方式簡(jiǎn)潔很多豆励,并且剔除了c代碼與Python的耦合代碼授滓,是c代碼通用性更好。
然后編寫swig接口聲明文件spam.i:
%module spam
%{
#include "spam.h"
%}
%include "spam.h"
%include "typemaps.i"
int system(const char* INPUT);
這是一段語(yǔ)言無(wú)關(guān)的模塊聲明肆糕,要?jiǎng)?chuàng)建一個(gè)叫spam的模塊,對(duì)system做一個(gè)聲明在孝,主要是聲明參數(shù)作為入?yún)⑹褂贸峡小H缓髨?zhí)行swig編譯程序:
>swig -c++ -python spam.i
swig會(huì)生成spam_wrap.cxx和spam.py兩個(gè)文件。先看spam_wrap.cxx私沮,這個(gè)生成的文件很長(zhǎng)始赎,但關(guān)鍵的就是對(duì)函數(shù)的包裝:
包裝函數(shù)傳入的還是PyObejct對(duì)象,內(nèi)部進(jìn)行了類型轉(zhuǎn)換仔燕,最終調(diào)了源碼中的system函數(shù)造垛。
生成的了另一個(gè)spam.py實(shí)際上是對(duì)so庫(kù)又用Python包裝了一層(實(shí)際比較多余):
這里使用_spam模塊,這里實(shí)際上是把擴(kuò)展命名為了_spam晰搀。關(guān)于swig在python上的應(yīng)用可以參見(jiàn):http://www.swig.org/Doc1.3/Python.html
下面就是編譯和安裝python 模塊五辽,Python提供了distutils module,可以很方便的編譯安裝python的module外恕。像下面這樣寫一個(gè)安裝腳本setup.py:
![](http://i2.51cto.com/images/blog/201810/07/6f294d1352d038e7cd1904283d5c5634.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
執(zhí)行 python setup.py build杆逗,即可以完成編譯乡翅,程序會(huì)創(chuàng)建一個(gè)build目錄,下面有編譯好的so庫(kù)罪郊。so庫(kù)放在當(dāng)前目錄下蠕蚜,其實(shí)Python就可以通過(guò)import來(lái)加載模塊了。當(dāng)然也可以用 python setup.py install 把模塊安裝到語(yǔ)言的擴(kuò)展庫(kù)——site-packages目錄中悔橄。關(guān)于build python擴(kuò)展靶累,可以參考https://docs.python.org/2/extending/building.html#building
混合編程性能分析
混合編程的使用場(chǎng)景中,很重要一個(gè)就是性能攸關(guān)癣疟。那么這小節(jié)將通過(guò)幾個(gè)小實(shí)驗(yàn)驗(yàn)證下混合編程的性能如何挣柬,或者說(shuō)怎樣寫程序能發(fā)揮好混合編程的性能優(yōu)勢(shì)。
我們使用冒泡排序算法來(lái)驗(yàn)證性能争舞。
1)實(shí)驗(yàn)一 使用冒泡程序驗(yàn)證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語(yǔ)言版冒泡排序
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;
}
}
使用一個(gè)長(zhǎng)度為100內(nèi)容固定的數(shù)組凛忿,反復(fù)排序10000次(每次排序后,再把數(shù)組恢復(fù)成原始序列)竞川,記錄執(zhí)行時(shí)間:
在相同的機(jī)器上多次執(zhí)行店溢,Python版執(zhí)行時(shí)間是10.3s左右,而c語(yǔ)言版本(未使用任何優(yōu)化編譯參數(shù))執(zhí)行時(shí)間只有0.29s左右委乌。相比之下Python的性能的確差很多(主要是Python中l(wèi)ist的操作跟c的數(shù)組相比床牧,效率差非常多),但Python中很多擴(kuò)展都是c語(yǔ)言寫的遭贸,目的就是為了提升效率戈咳,Python用于數(shù)據(jù)分析的numpy庫(kù)就擁有不錯(cuò)的性能。下個(gè)實(shí)驗(yàn)就驗(yàn)證壕吹,如果Python使用c語(yǔ)言版本的冒泡排序擴(kuò)展庫(kù)著蛙,性能會(huì)提升多少。
2)實(shí)驗(yàn)二 Python語(yǔ)言使用ctypes方式調(diào)用
這里直接使用c_int來(lái)定義了數(shù)組對(duì)象耳贬,這也節(jié)省了調(diào)用時(shí)數(shù)據(jù)類型轉(zhuǎn)換的開(kāi)銷:
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
再次執(zhí)行:
為了減少誤差踏堡,把循環(huán)增加到10萬(wàn)次,結(jié)果c原生程序使用優(yōu)化參數(shù)編譯后用時(shí)0.65s左右咒劲。Python使用c擴(kuò)展后(相同編譯參數(shù))執(zhí)行僅需2.3s左右顷蟆。
3)實(shí)驗(yàn)三 在c語(yǔ)言中使用PyObject處理入?yún)?/p>
這種方式是在Python中依然使用list裝入待排序數(shù)列,在c函數(shù)中把list賦值給數(shù)組腐魂,再進(jìn)行排序帐偎,排好序后,再對(duì)原始list賦值蛔屹。循環(huán)排序10萬(wàn)次削樊,執(zhí)行用時(shí)1.0s左右。
4) 實(shí)驗(yàn)四 使用swig來(lái)包裝c方法
在接口文件中聲明%array_class(int,intArray);然后在Python中使用initArray來(lái)作為數(shù)組兔毒,同樣修改成10萬(wàn)次排序嫉父。Python版本的程序(相同編譯參數(shù))執(zhí)行僅需0.7s左右沛硅,比c原生程序慢大概7%。
結(jié)論
1.Python的list效率非常低绕辖,在高性能場(chǎng)景下避免對(duì)list大量循環(huán)摇肌、取值、賦值操作仪际。如需要最好使用ctype中的數(shù)組围小,或者是用c語(yǔ)言來(lái)實(shí)現(xiàn)。
2.應(yīng)該把耗時(shí)的cpu密集型的邏輯交給c/c++實(shí)現(xiàn)树碱,Python使用擴(kuò)展即可肯适。