這篇文章想討論一個什么話題摄咆?我們討論一種方法——兼顧編譯語言的性能昨凡,同時又能保留動態(tài)語言的靈活性命满。我們想實現(xiàn)一個插件式框架童芹,框架結(jié)構(gòu)實用Python來實現(xiàn)命爬,插件使用c/c++來實現(xiàn)。這個框架可以實現(xiàn)插件的即插即用辐脖,插件的無損升級,插件的版本和依賴性管理皆愉,和靈活的擴展能力嗜价。
? ? ? ? 框架應(yīng)用在哪呢?在《基于Elasticsearch的數(shù)據(jù)分析系統(tǒng)(3)》中介紹了數(shù)據(jù)采集service幕庐,這個service目前僅對接一個設(shè)備久锥,未來會對接更多設(shè)備,接收處理不同的協(xié)議/格式异剥,數(shù)據(jù)切分成消息后除了送入kafka瑟由,也可能變更或通過復(fù)制的方式發(fā)到其他系統(tǒng),我們的框架要支持這些可變性冤寿。出于性能歹苦、資源節(jié)省等方面的考慮,我們沒有選用Flume督怜、Logstash等開源日志收集組件殴瘦。在二進制數(shù)據(jù)處理方面我們希望用編譯語言來快速處理。同理号杠,系統(tǒng)中的數(shù)據(jù)預(yù)處理servcie蚪腋,也有類似需求。
? ? ? ? 因為我們利用混合編程的優(yōu)勢來搭建框架姨蟋,所以我們從混合編程介紹起屉凯。
1.混合編程的應(yīng)用介紹
混合編程的應(yīng)用其實并不少見。最典型的是一款大名鼎鼎的網(wǎng)絡(luò)模擬軟件NS2(Network?Simulator)眼溶,最新版本是NS3悠砚。從事網(wǎng)絡(luò)研究、性能分析的人員對它可能都比較熟悉偷仿。
? ? ? ? NS2使用C++和Otcl作為開發(fā)語言哩簿,NS3使用C++和python宵蕉。之所以這樣設(shè)計就是出于性能和易用性的考慮。使用c++來寫網(wǎng)絡(luò)組件节榜,研究人員可以使用c++開發(fā)新的協(xié)議算法組件編譯進NS中羡玛。然后使用解釋型語言建模網(wǎng)絡(luò)——創(chuàng)建網(wǎng)絡(luò)組件及連接關(guān)系、網(wǎng)絡(luò)配置宗苍、網(wǎng)絡(luò)事件設(shè)置等稼稿。
? ? ? ? NS的組件使用OOP(面向?qū)ο缶幊?技術(shù)來實現(xiàn),組件的樣例代碼如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖1? Classifier的頭文件
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖2 tcp cubic算法實現(xiàn)
? ? ?圖1 中Classifier類是NS的地址分類器讳窟,負責(zé)將包轉(zhuǎn)發(fā)給下一級節(jié)點让歼,角色上相當(dāng)于路由器。圖2 是TCP cubic擁塞控制算法的實現(xiàn)丽啡,實際上就是從linux移植過來的谋右,是一個c代碼實現(xiàn)。這些內(nèi)部組件补箍,核心算法都是c/c++實現(xiàn)的改执,保證模擬時性能最優(yōu)。
? ? ?網(wǎng)絡(luò)建目友牛看上去是下面這個樣子的:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖3? ? ? tcl定義的模擬腳本
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖4? ? ? python定義的模擬腳本
可能有人覺得tcl辈挂、python的定義的模擬腳本看上去就像個配置文件,但跟配置文件不同的是裹粤,你可以在腳本中動態(tài)地修改模擬器內(nèi)部狀態(tài)终蒂。實際上,C/C++這部分就像一臺車的引擎遥诉,它負載快拇泣。而動態(tài)語言這部分就像控制臺,負載操作更方便矮锈、靈活挫酿。
2.C/C++與python混合編程
首先要說一下python只是一個語言規(guī)范,實際上python有很多實現(xiàn):CPython是標(biāo)準(zhǔn)Python愕难,是由C編寫的早龟,python腳本被編譯成CPython字節(jié)碼,然后由虛擬機解釋執(zhí)行猫缭,垃圾回收使用引用計數(shù)葱弟,我們談與C/C++混合編程實際指的是基于CPython解釋上的。除此之外猜丹,還有Jython芝加、IronPython、PyPy、Pyston藏杖,Jython是Java編寫的将塑,使用JVM的垃圾回收,可以與Java混合編程蝌麸,IronPython面向.NET平臺... ... 具體介紹可以參見這篇文章:http://python.jobbole.com/82703/
? ? ? ?python與C/C++混合編程的本質(zhì)是python調(diào)用C/C++編譯的動態(tài)鏈接庫点寥,關(guān)鍵就是把python中的數(shù)據(jù)類型轉(zhuǎn)換成c/c++中的數(shù)據(jù)類型,給編譯函數(shù)處理来吩,然后返回參數(shù)再轉(zhuǎn)換成python中的數(shù)據(jù)類型敢辩。但有幾種方式:
1)python中使用ctypes moduel,將python類型轉(zhuǎn)成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庫弟疆,在python中的調(diào)用:
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庫中的函數(shù)
ctypes的更多說明參見https://docs.python.org/2/library/ctypes.html戚长。使用ctypes可以在python與so之間通過指針傳遞更復(fù)雜的結(jié)構(gòu)體等。
2)在C/C++程序中使用Python.h怠苔,寫wrap包裝接口
? ? 這種方式是在c/c++程序中處理入/出參數(shù)同廉,先看一段例程(輸入字符串,然后當(dāng)做系統(tǒng)命令執(zhí)行):
Html 代碼
01#include <Python.h>
02static?PyObject* SpamError;
03static?PyObject* spam_system(PyObject* self, PyObject* args)
04{
05????????const?char* command;
06????????int?sts;
07????????if?(!PyArg_ParseTuple(args,?"s", &command))?//將args參數(shù)按照string類型處理柑司,給command賦值
08????????????????return?NULL;
09????????sts =?system(command);?//調(diào)用系統(tǒng)命令
10????????if?(sts < 0) {
11????????????????PyErr_SetString(SpamError,?"System command failed");
12????????????????return?NULL;
13????????}
14????????return?PyLong_FromLong(sts);?//將返回結(jié)果轉(zhuǎn)換為PyObject類型
15}
16//方法表
17static?PyMethodDef SpamMethods[] = {
18????????{"system", spam_system, METH_VARARGS,
19????????"Execute a shell command."},
20????????{NULL, NULL, 0, NULL}
21};
22//模塊初始化函數(shù)
23PyMODINIT_FUNC initspam(void)
24{
25????????PyObject* m;
26????????//m = PyModule_Create(&spammodule); // v3.4
27????????m = Py_InitModule("spam", SpamMethods);
28????????if?(m == NULL)
29????????????????return;
30????????SpamError = PyErr_NewException("spam.error",NULL,NULL);
31????????Py_INCREF(SpamError);
32????????PyModule_AddObject(m,"error",SpamError);
33}
? ? ? 處理上所有的入?yún)⑿羧堋⒊鰠⒍甲鳛镻yObject對象來處理,然后使用轉(zhuǎn)換函數(shù)把python的數(shù)據(jù)類型轉(zhuǎn)換成c/c++中的類型帜羊,返回參數(shù)按相同方式處理。比第一種方法多了初始化函數(shù)鸠天,這部分是把編譯的so庫當(dāng)做python module所必需要做的讼育。python中可以這樣使用:
Html 代碼
1imoprt spam
2spam.system("ls")
使用c/c++編寫python擴展的介紹可以參見:http://docs.python.org/2.7/extending/extending.html
3)使用SWIG,來生成獨立的wrap文件
這種方式并不能算是一種新方式稠集,實際上是基于第二中方式的一種包裝奶段。SWIG是個幫助使用C或者C++編寫的軟件能與其它各種高級編程語言進行嵌入聯(lián)接的開發(fā)工具。SWIG能應(yīng)用于各種不同類型的語言包括常用腳本編譯語言例如Perl, PHP, Python, Tcl, Ruby, PHP剥纷,C#,Java,R等痹籍。
? ? ? ?操作上,是針對c/c++程序編寫?yīng)毩⒌慕涌诼暶魑募ㄍǔ:芎唵危┗扌瑂wig會分析c/c++源程序自動分析接口要如何包裝蹲缠。在指定目標(biāo)語言后,swig會生成額外的包裝源碼文件悠垛。編譯so庫時线定,把包裝文件一起編譯、連接即可确买〗锛ィ看個例子:
Cpp 代碼
1int?system(const?char* command)
2{
3????????sts =?system(command);
4????????if?(sts < 0) {
5????????????????return?NULL;
6????????}
7????????return?sts;
8}
? ? ? 將c源碼中去掉適配python的包裝,僅定義system函數(shù)本身湾趾。然后編寫接口聲明文件spam.i:
Python 代碼
1%module spam
2%{
3#include "spam.h"
4%}
5%include?"spam.h"
6%include?"typemaps.i"
7int?system(const?char* INPUT);
? ? ? 聲明要創(chuàng)建一個叫spam的模塊芭商,對system做一個聲明派草,主要是聲明參數(shù)作為入?yún)⑹褂谩H缓髨?zhí)行:
Cpp 代碼
#swig -c++ -python spam.i
? ? ? swig會生成spam_wrap.cxx和spam.py兩個文件铛楣。先看spam_wrap.cxx近迁,這個生成的文件很長,但關(guān)鍵的就是對函數(shù)的包裝:
? ? ? ?包裝函數(shù)傳入的還是PyObejct對象蛉艾,內(nèi)部進行了類型轉(zhuǎn)換钳踊,最終調(diào)了源碼中的system函數(shù)。
? ? ? ?生成的了另一個spam.py實際上是對so庫又用python包裝了一層(實際比較多余):
這里使用_spam模塊,這里實際上是把擴展命名為了_spam。關(guān)于swig在python上的應(yīng)用可以參見:http://www.swig.org/Doc1.3/Python.html
? ? ? ?下面就是編譯和安裝python 模塊巩步,Python提供了distutils module郑叠,可以很方便的編譯安裝python的module。像下面這樣寫一個安裝腳本setup.py:
執(zhí)行? python setup.py build叶撒,即可以完成編譯,程序會創(chuàng)建一個build目錄,下面有編譯好的so庫蛆橡。so庫放在當(dāng)前目錄下,其實Python就可以通過import來加載模塊了掘譬。當(dāng)然也可以用 python setup.py install 把模塊安裝到語言的擴展庫——site-packages目錄中泰演。關(guān)于build python擴展,可以參考https://docs.python.org/2/extending/building.html#building
3. 混合編程的性能分析
? ? ? ?混合編程的使用場景中葱轩,很重要一個就是性能攸關(guān)睦焕。那么這小節(jié)將通過幾個小實驗驗證下混合編程的性能如何,或者說怎樣寫程序能發(fā)揮好混合編程的性能優(yōu)勢靴拱。
我們使用冒泡排序算法來驗證性能垃喊。
1)實驗一? ? 使用冒泡程序驗證python和c/c++程序的性能差距
python版冒泡程序:
Cpp 代碼
01def?bubble(arr,length):
02????j?=?length?-?1
03????while?j >=?0:
04????????i?=?0
05????????while?i < j:
06????????????if?arr[i] > arr[i+1]:
07????????????????tmp?=?arr[i+1]
08????????????????arr[i+1]?=?arr[i]
09????????????????arr[i]?=?tmp
10????????????i?+=?1
11????????j?-=?1
c語言版冒泡程序:
Python 代碼
01void?bubble(int* arr,int?length){
02????int?j = length - 1;
03????int?i;
04????int?tmp;
05????while(j >= 0){
06????????i = 0;
07????????while(i < j){
08????????????if(arr[i] > arr[i+1]){
09????????????????tmp = arr[i+1];
10????????????????arr[i+1] = arr[i];
11????????????????arr[i] = tmp;
12????????????}
13????????????i += 1;
14????????}
15????????j -= 1;
16????}
17}
? ? ?使用一個長度為100內(nèi)容固定的數(shù)組,反復(fù)排序10000次(每次排序后袜炕,再把數(shù)組恢復(fù)成原始序列)本谜,記錄執(zhí)行時間:
在相同的機器上多次執(zhí)行,Python版執(zhí)行時間是10.3s左右偎窘,而c語言版本(未使用任何優(yōu)化編譯參數(shù))執(zhí)行時間只有0.29s左右乌助。相比之下python的性能的確差很多(主要是python中l(wèi)ist的操作跟c的數(shù)組相比,效率差非常多)陌知,但python中很多擴展都是c語言寫的眷茁,目的就是為了提升效率,python用于數(shù)據(jù)分析的numpy庫就擁有不錯的性能纵诞。下個實驗就驗證上祈,如果python使用c語言版本的冒泡排序擴展庫,性能會提升多少。
2)實驗二? ? Python使用c/c++擴展庫登刺,驗證性能提升
? ? ? ?我們還要驗證上面三種混合方式籽腕,各自效率怎樣。
??1.使用ctypes module
? ? ? ? ?這里直接使用c_int來定義了數(shù)組對象纸俭,這也節(jié)省了調(diào)用時數(shù)據(jù)類型轉(zhuǎn)換的開銷:
Python 代碼
01import?time
02from?ctypes?import?*
03IntArray100?=?c_int?*?100
04arr?=?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,
05???????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,
06???????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)
07... ...
08if?__name__?==?"__main__":
09????libbubble?=?CDLL('libbubble.so')
10????time1?=?time.time()
11????for?i?in?xrange(100000):
12????????libbubble.initArr(arr1,arr,100)
13????????libbubble.bubble(arr1,100)
14????time2?=?time.time()
15????print?time2?-?time1
? ? ? 再次執(zhí)行:
為了減少誤差皇耗,把循環(huán)增加到10萬次,結(jié)果c原生程序執(zhí)行需要2.8s左右揍很,使用優(yōu)化參數(shù)編譯后用時0.65s左右郎楼。python使用c擴展后(相同編譯參數(shù))執(zhí)行僅需2.3s左右。
C 代碼
>gcc?-pthread?-fno-strict-aliasing?-g?-O2?-DNDEBUG?-g?-fwrapv?-O3?-Wall?-Wstrict-prototypes?-fPIC?bubble1.c?-o?bubble.o
?2.在c語言中使用PyObject處理入?yún)?/p>
? ? ? 這種方式是在python中依然使用list裝入待排序數(shù)列窒悔,在c函數(shù)中把list賦值給數(shù)組呜袁,再進行排序,排好序后简珠,再對原始list賦值阶界。循環(huán)排序10萬次,執(zhí)行用時1.0s左右聋庵。
?3.使用swig來包裝c方法
? ? ? 在接口文件中聲明%array_class(int,intArray);然后在Python中使用initArray來作為數(shù)組膘融,同樣修改成10萬次排序。python版本的程序(相同編譯參數(shù))執(zhí)行僅需0.7s左右祭玉,比c原生程序慢大概7%氧映。
? ? ? 【結(jié)論】
? ? ? ? 1.python 的list效率非常低,在高性能場景下避免對list大量循環(huán)脱货、取值岛都、賦值操作。如需要最好使用ctype中的數(shù)組蹭劈,或者是用c語言來實現(xiàn)。
? ? ? ? 2.應(yīng)該把耗時的cpu密集型的邏輯交給c/c++實現(xiàn)线召,python使用擴展即可铺韧。
?4.插件式框架設(shè)計
? ? ? ? 基于以上探索,參考NS3的實現(xiàn)方式缓淹,以數(shù)據(jù)收集Service為例來分析哈打。數(shù)據(jù)收集Service接收TCP/UDP的數(shù)據(jù)傳輸方式,應(yīng)用層協(xié)議支持自定義讯壶,不同的外設(shè)完全可以定義自己的協(xié)議解析邏輯料仗。我們定義每種解析為一個plugin,輸入為應(yīng)用層數(shù)據(jù)伏蚊,輸出為一個個Message立轧。轉(zhuǎn)換成Message之后要輸入給后續(xù)Plugin處理(可能是kafka,可能是ZeroMQ,也可能直接對接數(shù)據(jù)預(yù)處理Service)氛改,我們之定義這些最近的接口帐萎,輸入是一批Message,返回成功或失敗胜卤。
? ? ? ? 這些Plugin都可以使用c/c++實現(xiàn)疆导,因為這些數(shù)據(jù)處理使用指針會更快。
? ? ? ?主Service其實僅僅是**端口葛躏,對于tcp連接澈段,判斷該使用什么plugin來處理,然后新建一個線程舰攒,把socket交給這個線程败富。框架的公共程序要對socket等資源維護起來芒率,如支持線程池囤耳,資源回收,plugin熱替換時偶芍,可以將資源移交給新處理線程充择,實現(xiàn)無損升級等。像這些操作并不是性能攸關(guān)的匪蟀,就完全可以用python來實現(xiàn)椎麦。
? ? ? ? 以上可以應(yīng)用OOA、OOD的方法進一步分析設(shè)計材彪,然后使用c++來實現(xiàn)插件观挎。使用swig來包裝接口。使用時看起來像這個樣子:
Python 代碼
1import?epsn
2import?collector
3plug1?=?epsn.EpsnPlugin()
4mainService?=?collector.CollectServer()
5mainService.addPlugin(plug1)
6mainService.run()
?5.版本管理及依賴性管理
python有很成熟的包管理工具段化,可以方便地構(gòu)建嘁捷、安裝、移除显熏、升級雄嚣、版本管理、依賴性管理喘蟆。上文setup.py中的代碼就使用distutils包缓升,是python內(nèi)置的包管理庫,可以很方便地構(gòu)建包蕴轨。相對功能更強地有setpuptools支持依賴性的管理港谊,具體參見http://setuptools.readthedocs.io/en/latest/setuptools.html#building-and-distributing-packages-with-setuptools。文章不再做更多介紹橙弱。