溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

python+C、C++混合編程的應(yīng)用

發(fā)布時間:2020-06-25 07:51:25 來源:網(wǎng)絡(luò) 閱讀:19235 作者:benson00 欄目:編程語言

TIOBE每個月都會新鮮出爐一份流行編程語言排行榜,這里會列出最流行的20種語言。排序說明不了語言的好壞,反應(yīng)的不過是某個軟件開發(fā)領(lǐng)域的熱門程度。語言的發(fā)展不是越來越common,而是越來越專注領(lǐng)域。有的語言專注于簡單高效,比如python,內(nèi)建的list,dict結(jié)構(gòu)比c/c++易用太多,但同樣為了安全、易用,語言也犧牲了部分性能。在有些領(lǐng)域,比如通信,性能很關(guān)鍵,但并不意味這個領(lǐng)域的coder只能苦苦掙扎于c/c++的陷阱中,比如可以使用多種語言混合編程。

我看到的一個很好的Python與c/c++混合編程的應(yīng)用是NS3(Network Simulator3)一款網(wǎng)絡(luò)模擬軟件,它的內(nèi)部計算引擎需要用高性能,但在用戶建模部分需要靈活易用。NS3的選擇是使用C/C++來模擬核心部件和協(xié)議,用python來建模和擴展。

這篇文章介紹python和c/c++三種混合編程的方法,并對性能加以分析。

混合編程的原理

首先要說一下python只是一個語言規(guī)范,實際上python有很多實現(xiàn):CPython是標(biāo)準Python,是由C編寫的,python腳本被編譯成CPython字節(jié)碼,然后由虛擬機解釋執(zhí)行,垃圾回收使用引用計數(shù),我們談與C/C++混合編程實際指的是基于CPython解釋上的。除此之外,還有Jython、IronPython、PyPy、Pyston,Jython是Java編寫的,使用JVM的垃圾回收,可以與Java混合編程,IronPython面向.NET平臺。
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ù)類型。

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庫,使用下面的編譯指令

>gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC addbuf.c -o addbuf.o

最后編寫python代碼,使用ctypes庫,將python類型轉(zhuǎn)換成c語言需要的類型,然后傳參調(diào)用so庫函數(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庫中的函數(shù)

在C/C++程序中使用Python.h,寫wrap包裝接口

這種方法需要修改c/c++代碼,在外部函數(shù)中處理入/出參,適配python的參數(shù)。寫一段c代碼將外部入?yún)⒆鳛閟hell命令執(zhí)行:


         #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參數(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)?、出參都作為PyObject對象來處理,然后使用轉(zhuǎn)換函數(shù)把python的數(shù)據(jù)類型轉(zhuǎn)換成c/c++中的類型,返回參數(shù)按相同方式處理。比第一種方法多了初始化函數(shù),這部分是把編譯的so庫當(dāng)做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++編寫的軟件能與其它各種高級編程語言進行嵌入聯(lián)接的開發(fā)工具。SWIG能應(yīng)用于各種不同類型的語言包括常用腳本編譯語言例如Perl, PHP, Python, Tcl, Ruby, PHP,C#,Java,R等。

操作上,是針對c/c++程序編寫?yīng)毩⒌慕涌诼暶魑募ㄍǔ:芎唵危?,swig會分析c/c++源程序自動分析接口要如何包裝。在指定目標(biāo)語言后,swig會生成額外的包裝源碼文件。編譯so庫時,把包裝文件一起編譯、連接即可??磦€c代碼例子:

int system(const char* command)
{
        sts = system(command);
        if (sts < 0) {
                return NULL;
        }
        return sts;
}

c源碼中去掉適配python的包裝,僅定義system函數(shù)本身,這比第二種方式簡潔很多,并且剔除了c代碼與python的耦合代碼,是c代碼通用性更好。
然后編寫swig接口聲明文件spam.i:

%module spam
%{
#include "spam.h"
%}
%include "spam.h"
%include "typemaps.i"
int system(const char* INPUT);

這是一段語言無關(guān)的模塊聲明,要創(chuàng)建一個叫spam的模塊,對system做一個聲明,主要是聲明參數(shù)作為入?yún)⑹褂?。然后?zhí)行swig編譯程序:

>swig -c++ -python spam.i

swig會生成spam_wrap.cxx和spam.py兩個文件。先看spam_wrap.cxx,這個生成的文件很長,但關(guān)鍵的就是對函數(shù)的包裝:
python+C、C++混合編程的應(yīng)用
包裝函數(shù)傳入的還是PyObejct對象,內(nèi)部進行了類型轉(zhuǎn)換,最終調(diào)了源碼中的system函數(shù)。
生成的了另一個spam.py實際上是對so庫又用python包裝了一層(實際比較多余):
python+C、C++混合編程的應(yīng)用

這里使用_spam模塊,這里實際上是把擴展命名為了_spam。關(guān)于swig在python上的應(yīng)用可以參見:http://www.swig.org/Doc1.3/Python.html
下面就是編譯和安裝python 模塊,Python提供了distutils module,可以很方便的編譯安裝python的module。像下面這樣寫一個安裝腳本setup.py:

python+C、C++混合編程的應(yīng)用

執(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

混合編程性能分析

混合編程的使用場景中,很重要一個就是性能攸關(guān)。那么這小節(jié)將通過幾個小實驗驗證下混合編程的性能如何,或者說怎樣寫程序能發(fā)揮好混合編程的性能優(yōu)勢。

我們使用冒泡排序算法來驗證性能。

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內(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語言使用ctypes方式調(diào)用
這里直接使用c_int來定義了數(shù)組對象,這也節(jié)省了調(diào)用時數(shù)據(jù)類型轉(zhuǎn)換的開銷:

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萬次,結(jié)果c原生程序使用優(yōu)化參數(shù)編譯后用時0.65s左右。python使用c擴展后(相同編譯參數(shù))執(zhí)行僅需2.3s左右。
3)實驗三 在c語言中使用PyObject處理入?yún)?br/>這種方式是在python中依然使用list裝入待排序數(shù)列,在c函數(shù)中把list賦值給數(shù)組,再進行排序,排好序后,再對原始list賦值。循環(huán)排序10萬次,執(zhí)行用時1.0s左右。
4) 實驗四 使用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使用擴展即可。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI