楔子 首先 Python 中聲明變量的方式在 Cython 里面也是可以使用的,因?yàn)?Python 代碼也是合法的 Cython 代碼。
在 Cython 中,沒有類型化的動(dòng)態(tài)變量的行為和 Python 完全相同,通過賦值語句 b = a 讓 b 和 a 都指向同一個(gè)列表。在 a[3] = 42.0 之后,b[3] == 42.0 也是成立的,因此斷言成立。 即便后面將 a 修改了,也只是讓 a 指向了新的對(duì)象,調(diào)整相應(yīng)的引用計(jì)數(shù)。而對(duì) b 而言則沒有受到絲毫影響,因此 b 指向的依舊是一個(gè)列表。這是完全合法、并且有效的 Python 代碼。 而對(duì)于靜態(tài)類型變量,我們?cè)?Cython 中通過 cdef 關(guān)鍵字進(jìn)行聲明,比如:
上面除了變量的聲明之外,其它的使用方式和 Python 并無二致,當(dāng)然簡(jiǎn)單的賦值的話,基本上所有語言都是類似的。但是 Python 的一些內(nèi)置函數(shù)、類、關(guān)鍵字等等都是可以直接使用的,因?yàn)槲覀冊(cè)?Cython 中可以直接寫 Python 代碼,它是 Python 的超集。 但是有一點(diǎn)需要注意:我們上面創(chuàng)建的變量 i、j、k 是 C 中的類型(int、float 比較特殊,后面會(huì)解釋),其意義最終要遵循 C 的標(biāo)準(zhǔn)。 不僅如此,就連使用 cdef 聲明變量的方式也是按照 C 的標(biāo)準(zhǔn)來的。
而在函數(shù)內(nèi)部,cdef 也是要進(jìn)行縮進(jìn)的,它們聲明的變量也是一個(gè)局部變量。
并且 cdef 還可以使用類似于 Python 上下文管理器的方式。
所以使用 cdef 聲明變量非常簡(jiǎn)單,格式:cdef 類型 變量名。當(dāng)然啦,同時(shí)也可以賦上初始值。 一旦使用 cdef 靜態(tài)聲明,那么后續(xù)再給變量賦值的時(shí)候,就不能那么隨心所欲了,舉個(gè)例子:
也正是因?yàn)樵诰幾g階段就能檢測(cè)出類型,并分配好內(nèi)存,所以在執(zhí)行的時(shí)候速度才會(huì)快。 static 和 const 如果你了解 C 的話,那么思考一下:假設(shè)要在函數(shù)中返回一個(gè)局部變量的指針、并且外部在接收這個(gè)指針之后,還能訪問指針指向的值,這個(gè)時(shí)候該怎么辦呢?我們知道 C 函數(shù)中的變量是分配在棧上的(不使用 malloc 函數(shù),而是直接創(chuàng)建一個(gè)變量),函數(shù)結(jié)束之后變量對(duì)應(yīng)的值就被銷毀了,所以這個(gè)時(shí)候即使返回一個(gè)指針也是無意義的。 盡管有些時(shí)候,在返回指針之后還是能夠訪問指向的內(nèi)存,但這只是當(dāng)前使用的編譯器比較笨,在編譯時(shí)沒有檢測(cè)出來。如果是高級(jí)一點(diǎn)的編譯器,那么在訪問的時(shí)候會(huì)報(bào)出段錯(cuò)誤或者打印出一個(gè)錯(cuò)誤的值;而更高級(jí)的編譯器甚至連指針都不讓返回了,因?yàn)橹羔樦赶虻膬?nèi)存已經(jīng)被回收了,那還要這個(gè)指針做什么?因此指針都不讓返回了。 而如果想做到這一點(diǎn),那么只需要在聲明變量的同時(shí)在前面加上 static 關(guān)鍵字,比如 static int i,這樣的話 i 這個(gè)變量就不會(huì)被分配到棧區(qū),而是會(huì)被分配到數(shù)據(jù)區(qū)。數(shù)據(jù)區(qū)里變量的生命周期不會(huì)隨著函數(shù)的結(jié)束而結(jié)束,而是伴隨著整個(gè)程序。 但可惜的是,static 不是一個(gè)有效的 Cython 關(guān)鍵字,因此我們無法在 Cython 中聲明一個(gè) C 的 static 變量。 除了 static,在 C 中還有一個(gè) const,用來聲明常量。一旦使用 const聲明,比如 const int i = 3,那么這個(gè) i 在后續(xù)就不可以被修改了。而在 Cython 中,const 是支持的,但是它只能在定義函數(shù)參數(shù)的時(shí)候使用,在介紹函數(shù)的時(shí)候再聊。 所以 C 的 static 和 const 目前在 Cython 中就無需太關(guān)注了。 C 類型 我們上面聲明變量的時(shí)候,指定的類型是 int 和 float,而在 Python 和 C 里面都有 int 和 float,那么用的到底是誰的呢?其實(shí)上面已經(jīng)說了,用的是 C 的 int 和 float,至于原因,我們后面再聊。 而 Cython 可以使用的 C 類型不僅有 int 和 float,像 short, int, long, unsigned short, long long, size_t, ssize_t 等基礎(chǔ)類型都是支持的,聲明變量的方式均為 cdef 類型 變量名。聲明的時(shí)候可以賦初始值,也可以不賦初始值。 而除了基礎(chǔ)類型,還有指針、數(shù)組、定義類型別名、結(jié)構(gòu)體、共同體、函數(shù)指針等等也是支持的,我們后面細(xì)說。 Cython 的自動(dòng)類型推斷 Cython 還會(huì)對(duì)函數(shù)體中沒有進(jìn)行類型聲明的變量自動(dòng)執(zhí)行類型推斷,比如:for 循環(huán)里面全部都是浮點(diǎn)數(shù)相加,沒有涉及到其它類型的變量,那么 Cython 在自動(dòng)對(duì)變量進(jìn)行推斷的時(shí)候會(huì)發(fā)現(xiàn)這個(gè)變量可以被優(yōu)化為靜態(tài)類型的 double。
看一個(gè)簡(jiǎn)單的函數(shù):
在這個(gè)例子中,Cython 會(huì)將賦給變量 i、c、r 的值標(biāo)記為通用的 Python 對(duì)象。盡管這些對(duì)象的類型和 C 的類型具有高度的相似性,但 Cython 會(huì)保守地推斷 i 可能無法用 C 的整型表示(C 的整數(shù)有范圍,而 Python 沒有、可以無限大),因此會(huì)將其作為符合 Python 代碼語義的 Python 對(duì)象。 而對(duì)于 d = 2.0,則可以自動(dòng)推斷為 C 的 double,因?yàn)?Python 的浮點(diǎn)數(shù)對(duì)應(yīng)的值在底層就是使用一個(gè) double 來存儲(chǔ)的。所以最終對(duì)于開發(fā)者來講,變量 d 看似是一個(gè) Python 的對(duì)象,但是 Cython 在執(zhí)行的時(shí)候會(huì)將其視為 C 的 double 以提高性能。 這就是即使我們寫純 Python 代碼,Cython 編譯器也能進(jìn)行優(yōu)化的原因,因?yàn)闀?huì)進(jìn)行推斷。但是很明顯,我們不應(yīng)該讓 Cython 編譯器去推斷,而是明確指定變量的類型。 當(dāng)然如果非要 Cython 編譯器去猜,也是可以的,而且還可以通過 infer_types 編譯器指令,在一些可能會(huì)改變 Python 代碼語義的情況下給 Cython 留有更多的余地來推斷一個(gè)變量的類型。
這里出現(xiàn)了一個(gè)新的關(guān)鍵字 cimport,它的含義我們以后會(huì)說,目前只需要知道它和 import 關(guān)鍵字一樣,是用來導(dǎo)入模塊的即可。然后我們通過裝飾器 @cython.infer_types(True),啟動(dòng)了相應(yīng)的類型推斷,也就是給 Cython 留有更多的猜測(cè)空間。 當(dāng) Cython 支持更多推斷的時(shí)候,變量 i 會(huì)被類型化為 C 的整型;d 和之前一樣是 double,而 c 和 r 都是復(fù)數(shù)變量,復(fù)數(shù)則依舊使用 Python 的復(fù)數(shù)類型。 但是注意:并不代表啟用 infer_types 時(shí),就萬事大吉了;我們知道在不指定 infer_types 的時(shí)候,Cython 推斷類型顯然是采用最最保險(xiǎn)的方法、在保證程序正確執(zhí)行的情況下進(jìn)行優(yōu)化,不能為了優(yōu)化而導(dǎo)致程序出現(xiàn)錯(cuò)誤,顯然正確性和效率之間,正確性是第一位的。 而 C 的整型由于存在溢出的問題,所以 Cython 不會(huì)擅自使用。但是我們通過 infer_types 啟動(dòng)了更多的類型推斷,讓 Cython 在不改變語義的情況下使用 C 的類型。但是溢出的問題它不知道,所以在這種情況下是需要我們來負(fù)責(zé)確保不會(huì)出現(xiàn)溢出。 對(duì)于一個(gè)函數(shù)來說,如果啟動(dòng)這樣的類型推斷的話,我們可以使用 infer_types 裝飾器的方式。不過還是那句話,我們應(yīng)該手動(dòng)指定類型,而不是讓 Cython 編譯器去猜,因?yàn)槲覀兪谴a的編寫者,類型什么的我們自己最清楚。因此 infer_types 這個(gè)裝飾器,在工作中并不常用,而且想提高速度,就必須事先顯式地規(guī)定好變量的類型是什么。 小結(jié) 以上就是在 Cython 中如何靜態(tài)聲明一個(gè)變量,方法是使用 cdef 關(guān)鍵字。事先規(guī)定好類型是非常重要的,一旦類型確定了,那么生成的機(jī)器碼的數(shù)量會(huì)少很多,從而實(shí)現(xiàn)速度的提升。 而 C 類型的變量的運(yùn)算速度比 Python 要快很多,這也是為什么 int 和 float 會(huì)選擇 C 的類型。而除了 int 和 float,C 的其它類型在 Cython 中也是支持的,包括指針、結(jié)構(gòu)體、共同體這樣的復(fù)雜結(jié)構(gòu)。 但是 C 的整型有一個(gè)問題,就是它是有范圍的,在使用的時(shí)候我們要確保不會(huì)溢出。所以 Cython 在自動(dòng)進(jìn)行類型推斷的時(shí)候,只要有可能改變語義,就不會(huì)擅自使用 C 的整型,哪怕賦的整數(shù)非常小。這個(gè)時(shí)候可以通過 infer_types 裝飾器,留給 Cython 更多的猜測(cè)空間。 不過還是那句話,我們不應(yīng)該讓 Cython 編譯器去猜,是否溢出是由我們來確定的。如果能保證整數(shù)不會(huì)超過 int 所能表示的最大范圍,那么就將變量聲明為 int;如果 int 無法表示,那么就使用 long long;如果還無法表示,那就沒辦法了,只能使用 Python 的整型了。而使用 Python 整型的方式就是不使用 cdef,直接動(dòng)態(tài)聲明即可。 所以如果要將變量聲明為整型,那么直接使用 ssize_t 即可,等價(jià)于 long long。而在工作中,能超過 ssize_t 最大表示范圍的整數(shù)還是極少的。
再次強(qiáng)調(diào),事先規(guī)定好類型對(duì)速度的提升起著非常重要的作用。因此在聲明變量的時(shí)候,一定將類型指定好,特別是涉及到數(shù)值計(jì)算的時(shí)候。只不過此時(shí)使用的是 C 的類型,需要額外考慮整數(shù)溢出的情況,但如果將類型聲明為 ssize_t 的話,還是很少會(huì)發(fā)生溢出的。 以上就是 cdef 的用法,但是還沒有結(jié)束,下一篇文章我們來介紹更多與類型相關(guān)的內(nèi)容。 |
|