一、前言
Python語(yǔ)言是目前比較火的語(yǔ)言,很容易上手,對(duì)數(shù)據(jù)處理也比較友好,可以用幾行代碼就能進(jìn)行一些簡(jiǎn)單的數(shù)據(jù)處理工作。但是對(duì)于稍微大型的數(shù)值計(jì)算,或者一些涉及到大量循環(huán)的數(shù)值計(jì)算python的計(jì)算速度有點(diǎn)讓人失望。
即使是使用numpy對(duì)算法算法進(jìn)行優(yōu)化,能提升的空間都非常有限了,當(dāng)然網(wǎng)上有一行代碼就提升100倍這種帖子,就是使用numba,但用了這個(gè)之后感受并不是太好,對(duì)于一個(gè)簡(jiǎn)單循環(huán)或許可以加速,對(duì)于復(fù)雜的循環(huán)效果也許并不好(可能是我不會(huì)使用,對(duì)numba沒(méi)有太深入的研究)。
想要提升python的數(shù)值計(jì)算,目前看到比較好的解決方案就是用C給python寫(xiě)計(jì)算的lib,如果僅是數(shù)值計(jì)算,其實(shí)涉及到的C++語(yǔ)言的知識(shí)并不太多,基本就是for循環(huán),if判斷,以及一些STL庫(kù),個(gè)人覺(jué)得性價(jià)比還是挺高的。
Python調(diào)用C進(jìn)行數(shù)值計(jì)算最大難點(diǎn)就是如何進(jìn)行數(shù)據(jù)交換,也就是如何把輸入?yún)?shù)的指針傳遞給C,以及如何返回輸出參數(shù)指針給python。有大神早就將這些都封裝好了,就是使用pybind11。
二、pybind11簡(jiǎn)介
Pybind11定義了一些python數(shù)據(jù)結(jié)構(gòu)和C++的對(duì)應(yīng)關(guān)系,使得數(shù)據(jù)交換變得非常簡(jiǎn)單,接下來(lái)簡(jiǎn)單介紹數(shù)值計(jì)算主要用的一些pybind11用到的知識(shí)。
1)獲取pybind11
想要用到pybind11的功能當(dāng)然得有pybind11的代碼,網(wǎng)上可以直接下載到pybind11-master.rar文件,只需要下載到本地,然后解壓就可以了。
2)cpp基本結(jié)構(gòu)介紹
接下來(lái)直接就寫(xiě)代碼吧,用Visual Studio創(chuàng)建個(gè)cpp文件(不會(huì)安裝vs的可以自行百度安裝一下,這個(gè)不是本文重點(diǎn)就不介紹了),本文使用的是VS2015。
首先include一些頭文件。Emm…反正感覺(jué)會(huì)用的先include進(jìn)來(lái)應(yīng)該沒(méi)錯(cuò)了。

接下來(lái)include本文的重點(diǎn)也就是pybind11的頭文件,主要用到的是numpy.h和pybind11.h。

然后修改下命名空間,只是為了書(shū)寫(xiě)方便和import pandas as pd一個(gè)道理。

接下來(lái)要定義模塊的入口,主要用的是PYBIND11_MODULE。

其中calc就是在python調(diào)用時(shí)候的模塊名稱(chēng),m是在C++文件中的模塊實(shí)例,可以通過(guò)m.doc()給這個(gè)模塊寫(xiě)下文檔,一般就是這個(gè)模塊是干什么的,可以在python環(huán)境help該模塊名來(lái)查看,接下來(lái)通過(guò)m.def定義函數(shù)名,第一個(gè)參數(shù)是字符串,是在python調(diào)用時(shí)候的函數(shù)名,第二個(gè)參數(shù)是C++文件中的對(duì)應(yīng)函數(shù)名稱(chēng),三個(gè)參數(shù)是該函數(shù)的介紹。
以上寫(xiě)完之后就可以愉快的開(kāi)始寫(xiě)函數(shù)了,整個(gè)cpp的結(jié)構(gòu)大致就是下面這樣了。

分別是頭文件,命名空間,函數(shù)區(qū),函數(shù)導(dǎo)出區(qū)。本文也按照常規(guī)套路hello world一把。
3)編譯cpp成pyd文件
接下來(lái)需要通過(guò)編譯器將cpp文件編譯成python的pyd文件,首先要找到vs的x64本機(jī)工具命令提示符,本文以vs2015為例,在開(kāi)始菜單直接搜索vs2015會(huì)出來(lái)以下選項(xiàng)(前提是你已經(jīng)裝了正確安裝vs)。然后選擇VS2014 x64 本機(jī)工具命令提示符(注意一定要是x64的)。

打開(kāi)之后通過(guò)cd /d 路徑,這個(gè)命令將路徑切換到cpp所在路徑。然后輸入以下命令。

其中calc.cpp是待編譯的cpp文件,路徑1需要替換成前文獲取到的pybind11-master文件夾下的include文件夾的所在路徑,路徑2需要替換成python安裝路徑的include文件夾的所在路徑,路徑3替換成python安裝路徑下的libs文件夾的所在路徑,calc.pyd是生成的pyd名稱(chēng),需要和cpp中模塊名一致。

輸入上述代碼回車(chē)之后,編譯成功會(huì)有如下信息打印,同時(shí)在cpp所在路徑會(huì)產(chǎn)生四個(gè)文件,我們需要的只是后綴為.pyd的文件。
4)python執(zhí)行
之后打開(kāi)前文編譯所有的python環(huán)境來(lái)進(jìn)行測(cè)試,注意這里編譯的pyd不同python版本是不能公用的(即python3.5編譯出來(lái)的文件,python3.6并不能調(diào)用),有時(shí)同樣是3.6編譯出來(lái)的也不能使用,這個(gè)還沒(méi)研究是怎么一個(gè)兼容關(guān)系。
Python調(diào)用pyd文件,一種簡(jiǎn)單的方法是用sys模塊直接加入pyd文件所在路徑,就可以直接調(diào)用,或者也可拷貝pyd文件到python能找到的路徑下,比如python的安裝路徑下。然后執(zhí)行寫(xiě)好的函數(shù)就有如下結(jié)果。

好的到此咱們已經(jīng)完成了整個(gè)從下載所需文件,到cpp文件書(shū)寫(xiě),然后對(duì)cpp進(jìn)行編譯,最后在python執(zhí)行的全過(guò)程。
5)pybind11數(shù)據(jù)結(jié)構(gòu)介紹
在數(shù)值計(jì)算用的最多的結(jié)構(gòu)是array_t<>,可以是array_t<float>,array_t<int>或者array_t<double>,當(dāng)然用的最多的肯定是array_t<double>了。

以上是一個(gè)兩個(gè)矩陣輸入,同時(shí)輸出函數(shù)也是一個(gè)矩陣的函數(shù)聲明,在array_t里面封裝了數(shù)據(jù)矩陣的指針以及數(shù)據(jù)矩陣的大小。

通過(guò)以上方法獲得了兩個(gè)數(shù)據(jù)指針ptr1和ptr2,以及第一個(gè)矩陣的大小。
接下來(lái)定義輸出參數(shù),申請(qǐng)內(nèi)存并獲得數(shù)據(jù)指針。

上述簡(jiǎn)單介紹了py::array_t的基本用法,pybind11還定義了py::list等等數(shù)據(jù)封裝內(nèi)容,這些可以自行查看pybind11文檔。地址為https://pybind11.readthedocs.io/en/master/intro.html,或者相應(yīng)的pdf文檔。
6)讀取和數(shù)據(jù)存儲(chǔ)
為了方便代碼書(shū)寫(xiě),本文會(huì)獲得的指針進(jìn)行宏定義,使得代碼更有可讀性,這里就涉及到了數(shù)據(jù)存儲(chǔ)方式的問(wèn)題。

這里示范的書(shū)寫(xiě)方法是默認(rèn)輸入矩陣和輸出矩陣都是按行存儲(chǔ),這一點(diǎn)特別需要注意,其中numpy里的array數(shù)據(jù)默認(rèn)是按行存儲(chǔ),也就是不管何種存儲(chǔ)方式,只要對(duì)array數(shù)據(jù)進(jìn)行copy操作之后,返回的數(shù)據(jù)都是按行存儲(chǔ)。所以一般用array數(shù)據(jù)矩陣作為C函數(shù)輸入時(shí),進(jìn)行copy操作是比較穩(wěn)妥的方式,但是當(dāng)矩陣較大時(shí),進(jìn)行矩陣的深拷貝的速度往往會(huì)很慢,甚至可能大于計(jì)算所需要的時(shí)間。
Python常用的庫(kù)還有pandas,DataFrame數(shù)據(jù)的存儲(chǔ)默認(rèn)是按列存儲(chǔ),也就是從通過(guò)某個(gè)dataframe數(shù)據(jù).values的方法獲得的array數(shù)據(jù)矩陣,默認(rèn)是按列存儲(chǔ)。
那么如何知道一個(gè)array數(shù)據(jù)矩陣是按行存儲(chǔ)還是按列存儲(chǔ)呢,array數(shù)據(jù)有相應(yīng)參數(shù)進(jìn)行說(shuō)明。

Array數(shù)據(jù)矩陣的flag屬性下,有f_contiguous和c_contiguous這兩個(gè)布爾類(lèi)型的屬性,當(dāng)c_contiguous為真時(shí),矩陣是按行存儲(chǔ),當(dāng)f_contiguous為真時(shí),矩陣是按列存儲(chǔ)。其中f好像是表示Fortran語(yǔ)言,這種語(yǔ)言主要用來(lái)進(jìn)行科學(xué)計(jì)算,是按列存儲(chǔ),據(jù)網(wǎng)上說(shuō)超大型的數(shù)值計(jì)算都是用這種語(yǔ)言。c表示C語(yǔ)言,c語(yǔ)言是按行存儲(chǔ)。平時(shí)用的比較多的數(shù)值計(jì)算的還有matlab,matlab是按列存儲(chǔ)的。貌似對(duì)于面板數(shù)據(jù)來(lái)說(shuō),進(jìn)行時(shí)間序列上的操作確實(shí)是按列存儲(chǔ)比較占優(yōu)。


7)C++函數(shù)結(jié)構(gòu)
對(duì)于一個(gè)常用的輸入為矩陣,輸出也為矩陣的函數(shù)來(lái)說(shuō),大致的函數(shù)結(jié)構(gòu)如下,

分為輸入?yún)?shù)獲取,輸出參數(shù)定義,數(shù)據(jù)矩陣宏定義,運(yùn)算邏輯,以及結(jié)果的返回?;旧系暮瘮?shù)書(shū)寫(xiě),僅需要關(guān)注運(yùn)算邏輯如何書(shū)寫(xiě),其他幾部分內(nèi)容都是相對(duì)固定的。
三、后語(yǔ)
前文內(nèi)容詳細(xì)介紹了如何構(gòu)建整個(gè)cpp的內(nèi)容結(jié)構(gòu),C++函數(shù)的內(nèi)容結(jié)構(gòu),以及pybind11最常用的py::array_t的介紹。在給python寫(xiě)調(diào)用函數(shù)時(shí),只需要專(zhuān)注于C++函數(shù)內(nèi)容的運(yùn)算邏輯區(qū)的代碼即可,可以說(shuō)已經(jīng)非常簡(jiǎn)單了。
文中提到的pybind11-master.rar,pybind11用戶pdf文檔,關(guān)注【量化雜貨鋪】wx公眾號(hào)回復(fù)【加速一】就可以獲得。
這個(gè)只是本系列的第一篇介紹,后續(xù)有使得你運(yùn)行速度更快的詳細(xì)介紹,期待你的運(yùn)算速度可以起飛~~~~