作者:Anthony Shaw 是 Python 軟件基金會(huì)成員和 Apache 基金會(huì)成員。
近來Python可謂人氣驟升。這門編程語言用于開發(fā)運(yùn)維(DevOps)、數(shù)據(jù)科學(xué)、網(wǎng)站開發(fā)和安全。
然而,它沒有因速度而贏得任何獎(jiǎng)牌。
Java在速度方面與C、C++、C#或Python相比如何?答案很大程度上取決于你運(yùn)行的應(yīng)用程序的類型。沒有哪個(gè)基準(zhǔn)測(cè)試程序盡善盡美,不過The Computer Language Benchmarks Game(計(jì)算機(jī)語言基準(zhǔn)測(cè)試游戲)是個(gè)不錯(cuò)的起點(diǎn)。
十多年來,我一直提到計(jì)算機(jī)語言基準(zhǔn)測(cè)試游戲;與Java、C#、Go、Java和C++等其他語言相比,Python是速度最慢的語言之一。除了Java等解釋語言外,這還包括JIT(C#和Java)以及AOT(C和C++)編譯器。
注意:我說“Python”時(shí),其實(shí)指這種語言的參考實(shí)現(xiàn):CPython。我會(huì)在本文中提到其他運(yùn)行時(shí)環(huán)境。
我想回答這個(gè)問題:Python運(yùn)行完成類似的應(yīng)用程序比另一種語言慢2倍至10倍時(shí),為什么它這么慢,我們能不能讓它更快些?
下面是幾種常見的說法:
- “它是GIL(全局解釋器鎖)”
- “這是由于它是解釋的,而非編譯”
- “這是由于它是一種動(dòng)態(tài)類型語言”
那么,到底上述哪個(gè)原因?qū)π阅軒淼挠绊懽畲螅?
“它是GIL”
現(xiàn)代計(jì)算機(jī)搭載擁有多個(gè)內(nèi)核的CPU,有時(shí)搭載多個(gè)處理器。為了利用所有這些額外的處理能力,操作系統(tǒng)定義了一種名為線程的低級(jí)結(jié)構(gòu):一個(gè)進(jìn)程(比如Chrome瀏覽器)可能生成多個(gè)線程,并擁有針對(duì)內(nèi)部系統(tǒng)的指令。這樣一來,如果某個(gè)進(jìn)程特別耗費(fèi)CPU資源,該負(fù)載可以在諸多核心之間分擔(dān),這實(shí)際上讓大多數(shù)應(yīng)用程序更快地完成任務(wù)。
我在寫這篇文章時(shí),我的Chrome瀏覽器有44個(gè)線程開著。請(qǐng)記住這點(diǎn):線程的結(jié)構(gòu)和API在基于POSIX的操作系統(tǒng)(比如Mac OS和Linux)與Windows OS之間是不同的。操作系統(tǒng)還處理線程的調(diào)度。
如果你之前沒有從事過多線程編程,需要盡快熟悉的一個(gè)概念就是鎖(lock)。與單線程進(jìn)程不同,當(dāng)你需要確保改變內(nèi)存中的變量時(shí),多個(gè)線程并不同時(shí)試圖訪問/改變同樣的內(nèi)存地址。
CPython創(chuàng)建變量時(shí),它會(huì)分配內(nèi)存,然后計(jì)算該變量的引用有多少,這個(gè)概念名為引用計(jì)數(shù)(reference counting)。如果引用數(shù)為0,那么它從系統(tǒng)釋放這部分內(nèi)存。這就是為什么在某個(gè)代碼段(比如for循環(huán)的范圍)內(nèi)創(chuàng)建一個(gè)“臨時(shí)”變量不會(huì)搞砸應(yīng)用程序的內(nèi)存消耗。
當(dāng)變量在多個(gè)線程內(nèi)共享時(shí),就出現(xiàn)了這個(gè)難題:CPython如何鎖定引用計(jì)數(shù)。有一個(gè)“全局解釋器鎖”,它小心地控制線程執(zhí)行。解釋器一次只能執(zhí)行一個(gè)操作,無論它有多少線程。
這對(duì)Python應(yīng)用程序的性能來說意味著什么?
如果你有單線程、單個(gè)解釋器的應(yīng)用程序,這對(duì)速度不會(huì)有影響。刪除GIL根本不會(huì)影響你代碼的性能。
如果你想通過使用線程機(jī)制在單個(gè)解釋器(Python進(jìn)程)內(nèi)實(shí)現(xiàn)并發(fā)功能,而且線程是IO密集型(比如網(wǎng)絡(luò)IO或磁盤IO),你會(huì)看到GIL爭(zhēng)奪的后果。
上圖來自大衛(wèi)?比茲利(David Beazley)撰寫的《GIL可視化》文章:http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html
如果你有Web應(yīng)用程序(比如Django),又在使用WSGI,那么針對(duì)Web應(yīng)用程序的每個(gè)請(qǐng)求都是一個(gè)單獨(dú)的Python解釋器,所以每個(gè)請(qǐng)求只有一個(gè)鎖。由于Python解釋器啟動(dòng)緩慢,一些WSGI實(shí)現(xiàn)擁有“守護(hù)進(jìn)程模式”,這可以讓一個(gè)或多個(gè)Python進(jìn)程為你保持活躍狀態(tài)。
其他Python運(yùn)行時(shí)環(huán)境怎么樣?
PyPy有一個(gè)GIL,它通常比CPython快3倍。
Jython之所以沒有GIL,是由于Jython中的Python線程由Java線程表示,受益于JVM內(nèi)存管理系統(tǒng)。
Java如何執(zhí)行此任務(wù)?
好吧,首先所有Java引擎都使用標(biāo)記-清除(mark-and-sweep)垃圾收集機(jī)制。如上所述,GIL的主要需求是CPython的內(nèi)存管理算法。
Java沒有GIL,但它也是單線程的,所以它不需要內(nèi)存管理算法。Java的事件循環(huán)和承諾回調(diào)(Promise/Callback)模式是實(shí)現(xiàn)異步編程以代替并發(fā)的方法。Python與asyncio事件循環(huán)有相似之處。
“這是由于它一種解釋語言”
我常聽到這個(gè)觀點(diǎn),但覺得這過于簡(jiǎn)化了CPython的實(shí)際工作方式。如果你在終端上編寫了python my.py,那么CPython會(huì)啟動(dòng)讀取、分析、解析、編譯、解釋和執(zhí)行代碼的一長(zhǎng)串操作。
如果你對(duì)這個(gè)過程的機(jī)理頗感興趣,我之前寫過一篇文章:《6分鐘內(nèi)修改Python語言》(https://hackernoon.com/modifying-the-python-language-in-7-minutes-b94b0a99ce14)。
這個(gè)過程的一個(gè)重要節(jié)點(diǎn)是創(chuàng)建.pyc文件;在編譯階段,字節(jié)碼序列寫入到Python 3中__pycache__/里面的一個(gè)文件或Python 2中的同一個(gè)目錄。這不僅適用于你的腳本,還適用于導(dǎo)入的所有代碼,包括第三方模塊。
所以在大部分時(shí)間(除非你編寫的是只運(yùn)行一次的代碼?),Python解釋字節(jié)碼,并在本地執(zhí)行。相比之下Java和C#.NET:
Java編譯成一種“中間語言”,Java虛擬機(jī)讀取字節(jié)碼,并即時(shí)編譯成機(jī)器碼。.NET CIL也一樣,.NET公共語言運(yùn)行時(shí)環(huán)境(CLR)使用即時(shí)編譯,將編譯后代碼編譯成機(jī)器碼。
那么,既然都使用虛擬機(jī)和某種字節(jié)碼,為什么Python在基準(zhǔn)測(cè)試中比Java和C#都要慢得多呢?首先,.NET和Java是JIT編譯型的。
JIT或即時(shí)編譯需要一種中間語言,以便將代碼拆分成塊(或幀)。提前(AOT)編譯器旨在確保CPU在任何交互發(fā)生之前能理解每一行代碼。
JIT本身不會(huì)使執(zhí)行變得更快,因?yàn)樗匀粓?zhí)行相同的字節(jié)碼序列。然而,JIT讓代碼在運(yùn)行時(shí)能夠加以優(yōu)化。一個(gè)好的JIT優(yōu)化器會(huì)看到應(yīng)用程序的哪些部分在頻繁執(zhí)行,這些代碼稱之為“熱點(diǎn)代碼”(hot spot)。然后,它會(huì)對(duì)這些代碼進(jìn)行優(yōu)化,其辦法是把它們換成更高效的版本。
這就意味著當(dāng)你的應(yīng)用程序一次又一次地執(zhí)行相同的操作時(shí),運(yùn)行速度可以顯著加快。另外記住一點(diǎn):Java和C#是強(qiáng)類型語言,因此優(yōu)化器可以對(duì)代碼做出多得多的假設(shè)。
PyPy有JIT,如上所述,其速度比CPython快得多。這篇性能基準(zhǔn)測(cè)試文章作了更詳細(xì)的介紹:《哪個(gè)Python版本的速度最快?》(https://hackernoon.com/which-is-the-fastest-version-of-python-2ae7c61a6b2b)。
那么,CPython為什么不使用JIT呢?
JIT存在幾個(gè)缺點(diǎn):缺點(diǎn)之一是啟動(dòng)時(shí)間。CPython的啟動(dòng)時(shí)間已經(jīng)比較慢了,PyPy的啟動(dòng)時(shí)間比CPython還要慢2倍至3倍。眾所周知,Java虛擬機(jī)的啟動(dòng)速度很慢。.NET CLR通過系統(tǒng)開啟時(shí)啟動(dòng)解決了這個(gè)問題,但CLR的開發(fā)人員還開發(fā)了操作系統(tǒng),CLR在它上面運(yùn)行。
如果你有一個(gè)Python進(jìn)程長(zhǎng)時(shí)間運(yùn)行,代碼因含有“熱點(diǎn)代碼”而可以優(yōu)化,那么JIT大有意義。
然而,CPython是一種通用實(shí)現(xiàn)。所以,如果你在使用Python開發(fā)命令行應(yīng)用程序,每次調(diào)用CLI都得等待JIT啟動(dòng)會(huì)慢得要命。
CPython不得不試圖滿足盡可能多的用例(use case)。之前有人試過將JIT插入到CPython中,但這個(gè)項(xiàng)目基本上擱淺了。
如果你想獲得JIT的好處,又有適合它的工作負(fù)載,不妨使用PyPy。
“這是由于它是一種動(dòng)態(tài)類型語言”
在“靜態(tài)類型”語言中,你在聲明變量時(shí)必須指定變量的類型。這樣的語言包括C、C++、Java、C#和Go。
在動(dòng)態(tài)類型語言中,仍然存在類型這個(gè)概念,但變量的類型是動(dòng)態(tài)的。
在這個(gè)示例中,Python創(chuàng)建了一個(gè)有相同名稱、str類型的第二個(gè)變量,并釋放為a的第一個(gè)實(shí)例創(chuàng)建的內(nèi)存。
靜態(tài)類型語言不是為了給你添堵而設(shè)計(jì)的,它們是兼顧C(jī)PU的運(yùn)行方式設(shè)計(jì)的。如果一切最終需要等同于簡(jiǎn)單的二進(jìn)制操作,你就得將對(duì)象和類型轉(zhuǎn)換成低級(jí)數(shù)據(jù)結(jié)構(gòu)。
Python為你這么做這項(xiàng)工作,你永遠(yuǎn)看不到,也不需要操心。
不必聲明類型不是導(dǎo)致Python速度慢的原因,Python語言的設(shè)計(jì)使你能夠讓幾乎一切都是動(dòng)態(tài)的。你可以通過猴子補(bǔ)丁(monkey-patch),加入對(duì)運(yùn)行時(shí)聲明的值進(jìn)行低級(jí)系統(tǒng)調(diào)用的代碼。幾乎一切都有可能。
正是這種設(shè)計(jì)使得優(yōu)化Python異常困難。
為了說明我的觀點(diǎn),我將使用可在Mac OS中使用的一種名為Dtrace的系統(tǒng)調(diào)用跟蹤工具。CPython發(fā)行版并未內(nèi)置DTrace,所以你得重新編譯CPython。我使用3.6.6進(jìn)行演示。
現(xiàn)在python.exe將在整個(gè)代碼中使用Dtrace跟蹤器。保羅?羅斯(Paul Ross)寫了一篇關(guān)于Dtrace的雜談(https://github.com/paulross/dtrace-py#the-lightning-talk)。你可以下載Python的DTrace啟動(dòng)文件(https://github.com/paulross/dtrace-py/tree/master/toolkit)來測(cè)量函數(shù)調(diào)用、執(zhí)行時(shí)間、CPU時(shí)間、系統(tǒng)調(diào)用和各種好玩的指標(biāo)。比如
py_callflow跟蹤器顯示你應(yīng)用程序中的所有函數(shù)調(diào)用。
那么,Python的動(dòng)態(tài)類型會(huì)讓它變慢嗎?
- 比較和轉(zhuǎn)換類型的開銷很大,每次讀取、寫入或引用一個(gè)變量,都要檢查類型。
- 很難優(yōu)化一種極具動(dòng)態(tài)性的語言。Python的許多替代語言之所以快得多,原因在于它們?yōu)榱诵阅茉陟`活性方面作出了犧牲。
- Cython 結(jié)合了C-Static類型和Python來優(yōu)化類型已知的代碼,可以將性能提升84倍。
結(jié)論
Python之所以速度慢,主要是由于動(dòng)態(tài)性和多功能性。它可用作解決各種問題的工具,Python有更優(yōu)化、速度更快的幾個(gè)替代方案。
然而,有一些方法可以優(yōu)化你的Python應(yīng)用程序,比如通過充分利用異步、深入了解分析工具以及考慮使用多個(gè)解釋器。
對(duì)于啟動(dòng)時(shí)間不重要、代碼會(huì)受益于JIT的應(yīng)用程序來說,不妨考慮PyPy。
對(duì)于性能至關(guān)重要,又有更多靜態(tài)類型變量的部分代碼而言,不妨考慮使用Cython。
本文轉(zhuǎn)載自:https://465914.kuaizhan.com/36/57/p543294672f5b80
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
