我們先來看看,python之類語言的for循環(huán)获询,和其它語言相比涨岁,額外付出了什么。
我們知道吉嚣,python是解釋執(zhí)行的。
舉例來說蹬铺,執(zhí)行 x = 1234+5678 尝哆,對編譯型語言,是從內(nèi)存讀入兩個short int到寄存器甜攀,然后讀入加法指令秋泄,通知CPU內(nèi)部的加法器動作,最后把加法器輸出存儲到x對應(yīng)的內(nèi)存單元(實質(zhì)上规阀,最后這個動作幾乎總會被自動優(yōu)化為“把加法器輸出暫存到寄存器而不是內(nèi)存單元恒序,因為訪問內(nèi)存的時間消耗常常是訪問寄存器的幾十倍”)。一共2~4條指令(視不同CPU指令集而定)谁撼。
換了解釋性語言歧胁,情況就大大不同了。
它得先把“x = 1234+5678”當成字符串厉碟,逐個字符比對以分析語法結(jié)構(gòu)——不計空格這也是11個字符喊巍,至少要做11個循環(huán);每個循環(huán)至少需要執(zhí)行的指令有:取數(shù)據(jù)(如讀'x'這個字符)箍鼓、比較數(shù)據(jù)崭参、根據(jù)比較結(jié)果跳轉(zhuǎn)(可能還得跳轉(zhuǎn)回來)、累加循環(huán)計數(shù)器款咖、檢查循環(huán)計數(shù)器是否到達終值何暮、根據(jù)比較結(jié)果跳轉(zhuǎn)。這就是至少6條指令铐殃,其中包含一次內(nèi)存讀取海洼、至少兩次分支指令(現(xiàn)代CPU有分支預(yù)測,若命中無額外消耗背稼,否則……)贰军。總計66條指令,比編譯型語言慢至少17倍(假設(shè)每條指令執(zhí)行時間相同词疼。但事實上俯树,訪存/跳轉(zhuǎn)類指令消耗的時間常常是加法指令的十倍甚至百倍)。
這還只是讀入源碼的消耗贰盗,尚未計入“語法分析”這個大頭许饿;加上后,起碼指令數(shù)多數(shù)百倍(消耗時間嘛……我猜起碼得多數(shù)千倍吧)舵盈。
不過陋率,python比起其它解釋性語言還是強很多的。因為它可以事先把文本代碼編譯成“字節(jié)碼”(存儲于擴展名為pyc的文件里)秽晚,從而直接處理整型的“指令代碼”瓦糟,不再需要從頭開始分析文本。
但是赴蝇,從“字節(jié)碼”翻譯到實際CPU代碼這步菩浙,仍然是省不下的。
這個消耗句伶,可看作“利用虛擬機”執(zhí)行異構(gòu)CPU上的程序劲蜻。有人證明過,哪怕優(yōu)化到極致考余,這也需要10倍的性能消耗先嬉。
這個消耗也有辦法縮減。這就是JIT技術(shù)楚堤。
JIT說白了疫蔓,就是在第一遍執(zhí)行一段代碼前,先執(zhí)行編譯動作钾军,然后執(zhí)行編譯后的代碼鳄袍。
如果代碼中沒有循環(huán),那么這將白白付出很多額外的時間代價吏恭;但若有一定規(guī)模以上的循環(huán)拗小,就可能節(jié)省一點時間。
這里面的佼佼者是Java樱哼。它甚至能根據(jù)上次運行結(jié)果實時profile哀九,然后花大力氣優(yōu)化關(guān)鍵代碼,從而得到比C更快的執(zhí)行速度搅幅。
不過阅束,理想很豐滿,現(xiàn)實很骨感茄唐。雖然局部熱點的確可能更快息裸,但Java的整體效率仍然比C/C++差上很多——這個原因就比較復(fù)雜了蝇更。
和C/C++/Java那種投入海量資源經(jīng)過千錘百煉的編譯器不同,python的JIT甚至可稱得上“蹩腳”呼盆。
加加減減年扩,僅一個循環(huán),慢上十幾甚至幾十倍還是很正常的访圃。
以上討論厨幻,僅僅考慮了for循環(huán)這個控制結(jié)構(gòu)本身。事實上腿时,“慢”往往是全方位的况脆。
舉例來說,要計算一組向量批糟,首先就要存儲它格了。
怎么存儲呢?
對C/C++來說徽鼎,就存在“數(shù)組”里笆搓;而它的數(shù)組,就是赤裸裸的一片連續(xù)內(nèi)存區(qū)域纬傲;區(qū)域中每若干個字節(jié)就存儲了一個數(shù)值數(shù)據(jù)。
這種結(jié)構(gòu)CPU處理起來最為方便快捷肤频,且cache友好(若cache不友好就可能慢數(shù)倍甚至數(shù)十倍)叹括。
Java等其它語言就要稍遜一籌。因為它的“數(shù)組”是“真正的數(shù)組”宵荒;相對于“連續(xù)內(nèi)存區(qū)域”汁雷,“真正的數(shù)組”就不得不在每次訪問時檢查數(shù)組下標有無越界。這個檢查開銷不大报咳,但也不小……
當然侠讯,這也是有好處的。至少不用像C/C++那樣暑刃,整天擔心緩沖區(qū)溢出了厢漩。
而python之類……
為了遷就初學(xué)者,它去掉了“變量聲明”以及“數(shù)據(jù)類型”——于是它的用戶再也用不著岩臣、也沒法寫 int xxx了溜嗜。隨便什么數(shù)據(jù),咱想存就存架谎,烏拉炸宵!
但是,如果我告訴你谷扣,可變數(shù)據(jù)類型其實在C/C++里面是這樣聲明的呢:
typedef struct tagVARIANT {
union {
struct __tagVARIANT {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
LONGLONG llVal;
LONG lVal;
BYTE bVal;
SHORT iVal;
FLOAT fltVal;
DOUBLE dblVal;
VARIANT_BOOL boolVal;
_VARIANT_BOOL bool;
SCODE scode;
CY cyVal;
DATE date;
BSTR bstrVal;
IUnknown *punkVal;
IDispatch *pdispVal;
SAFEARRAY *parray;
BYTE *pbVal;
SHORT *piVal;
LONG *plVal;
LONGLONG *pllVal;
FLOAT *pfltVal;
DOUBLE *pdblVal;
VARIANT_BOOL *pboolVal;
_VARIANT_BOOL *pbool;
SCODE *pscode;
CY *pcyVal;
DATE *pdate;
BSTR *pbstrVal;
IUnknown **ppunkVal;
IDispatch **ppdispVal;
SAFEARRAY **pparray;
VARIANT *pvarVal;
PVOID byref;
CHAR cVal;
USHORT uiVal;
ULONG ulVal;
ULONGLONG ullVal;
INT intVal;
UINT uintVal;
DECIMAL *pdecVal;
CHAR *pcVal;
USHORT *puiVal;
ULONG *pulVal;
ULONGLONG *pullVal;
INT *pintVal;
UINT *puintVal;
struct __tagBRECORD {
PVOID pvRecord;
IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
} VARIANT, *LPVARIANT, VARIANTARG, *LPVARIANTARG;
簡單說土全,這玩意兒的思路就是“利用一個tag指示數(shù)據(jù)類型,真正的數(shù)據(jù)存儲在下面的union里;訪問時裹匙,依據(jù)tag指示轉(zhuǎn)換/返回合適類型”瑞凑。
很顯然,對C/C++/Java程序員來說幻件,這玩意兒無論時間還是空間上拨黔,都是個災(zāi)難。
并且绰沥,它也極度的cache不友好——本來可以連續(xù)存儲的篱蝇,現(xiàn)在……變成了個結(jié)構(gòu)體;而且一旦存了某些類型的數(shù)據(jù)徽曲,就不得不通過指針跳轉(zhuǎn)到另一塊區(qū)域才能訪問(如果原地存儲零截,浪費的空間就太恐怖了)。
所以你看秃臣,咱要基于這種結(jié)構(gòu)談效率涧衙,是不是有點……
哪怕僅僅了解到這個程度也已經(jīng)很是觸目驚心了:解釋執(zhí)行+字節(jié)碼優(yōu)化慢上至少10倍到幾十上百倍,“初學(xué)者友好”的基礎(chǔ)數(shù)據(jù)又慢上幾倍到幾十倍奥此,透過容器訪問(而非性能更好的弧哎、固定大小數(shù)組乃至不檢查下標假裝自己是數(shù)組的“內(nèi)存區(qū)域”)再慢上幾倍到幾十倍……哪怕咱暫時不考慮其它機制帶來的開銷,僅把這幾樣往一塊一湊(在某些特定的情況下稚虎,這些不同的“慢”點還可能相互影響撤嫩、起到“遲緩度倍增放大”的效果)……
除此之外,還有python內(nèi)部如何管理/索引/訪問腳本中的全局/局部變量的問題(一般會用dict)蠢终、用戶數(shù)據(jù)和物理機存儲器嚴重不匹配引起的緩存未命中問題序攘、python內(nèi)部狀態(tài)機/執(zhí)行現(xiàn)場管理等等方面管理的問題——對編譯型語言,這些統(tǒng)統(tǒng)不存在寻拂,CPU/內(nèi)存自己就把自己照顧的很好了程奠;但對解釋性語言,這些都會成為“遲緩度倍增”的元兇祭钉。
這些東西的相互影響極為復(fù)雜微妙瞄沙,幾乎沒人能徹底搞明白它。
你看朴皆,明白了前因后果帕识,咱是不是只能說“python的優(yōu)化實在不錯,才僅僅慢了20萬倍而已”呢遂铡?(笑~
當然肮疗,如果不做這類較為復(fù)雜的處理,僅僅是一些流程性的東西的話扒接,這類語言的處理速度還是夠用的——至少與之交互的人感受不到絲毫延遲伪货。
甚至们衙,哪怕需要復(fù)雜的處理,這類語言也可以向其它語言求救啊碱呼。就好像有個numpy蒙挑,誰敢說python做不了向量運算呢?
——當然愚臀,和行家說話時忆蚀,你得明白,這是找C之類語言搬救兵了姑裂。睜眼說瞎話把它當成python語言自己的能力是有點丟人的馋袜。不過如果只混python的圈子的話,這倒也不耽誤什么舶斧。
————————————————————————————
如果要揭短欣鳖,專業(yè)程序員還會把無數(shù)據(jù)類型導(dǎo)致接口模糊所以無法寫較為復(fù)雜的程序之類弊端給你列出一火車的。但這些就是沒必要的題外話了茴厉。
畢竟泽台,python只是個膠水語言,初學(xué)者友好并且應(yīng)付常見的簡單應(yīng)用場景綽綽有余矾缓,這已經(jīng)足夠了怀酷。
就好像把office做的傻瓜化,本就是專業(yè)程序員的工作一樣——用戶覺得好用嗜闻、樂意掏錢就行了胰坟,何必關(guān)心“做出一套office需要砸進去的錢足夠蓋N座迪拜塔”呢。
當然泞辐,如果想進一步發(fā)展的話,請記住“在合適的地方用合適的工具”這句話——然后想辦法搞明白每種工具的局限性吧竞滓。
畢竟咐吼,哪怕是C/C++,在做矩陣之類運算時商佑,也還會求助于SIMD的MMX指令锯茄、超線程/多核心CPU乃至GPU,以便為自己“增補”上并行處理能力呢茶没。