09.01 人生苦短,請用Python

Python是一門近幾年崛起很快也很火的編程語言。區別於我們國家大學裡的第一門編程語言往往是C語言,Python則成為了國外本科學的入門級編程語言。同時隨著科技的發展,擁有高容量、高速度和多樣性的大數據已經成為當今時代的主題詞。移動互聯網、雲計算、大數據的快速發展,使Python給開發者帶來巨大的機會。程序員界流行一句話:人生苦短,請用Python。隨著Python成為網紅語言之後,不少程序員想多學這一門語言好傍身。甚至有些27、28歲的程序員開始考慮要從安卓轉行做Python。這是一個非常有趣的現象:之前安卓比較火熱,很多人從java轉到安卓。現在人工智能也開始火了,又開始準備轉python。想要學會python,不僅要學習相關的基礎知識和教程,對python各種工具的熟悉使用才能讓你在工作中迅速成長!

人生苦短,請用Python

Python有三大神器:numpy,scipy,matplotlib,其中numpy很多底層使用C語言實現的,所以速度很快,用它參加各種數學建模大賽,完全可以替代r語言和MATLAB。spark,Hadoop都開了Python的接口,所以使用Python做大數據的mapreduce也非常簡單,加上py對數據庫支持都很好,或者類似sqlalchemy的orm也非常強大好用。

我們都知道谷歌製作出了的機器人戰勝了一個圍棋大師,這個就是目前剛出頭的人工智能,當然我們的人工智能時代還沒有到來,如果這天來了,生活和世界將會發生翻天覆地的變化,而且現在發展這麼快,人工智能的時代不會太遠。近來Python可謂人氣驟升。這門編程語言用於開發運維(DevOps)、數據科學、網站開發和安全。

然而,它沒有因速度而贏得任何獎牌。

人生苦短,請用Python

Java在速度方面與C、C++、C#或Python相比如何?答案很大程度上取決於你運行的應用程序的類型。沒有哪個基準測試程序盡善盡美,不過The Computer Language Benchmarks Game(計算機語言基準測試遊戲)是個不錯的起點。

十多年來,我一直提到計算機語言基準測試遊戲;與Java、C#、Go、JavaScript和C++等其他語言相比,Python是速度最慢的語言之一。除了JavaScript等解釋語言外,這還包括JIT(C#和Java)以及AOT(C和C++)編譯器。

注意:我說“Python”時,其實指這種語言的參考實現:CPython。我會在本文中提到其他運行時環境。

我想回答這個問題:Python運行完成類似的應用程序比另一種語言慢2倍至10倍時,為什麼它這麼慢,我們能不能讓它更快些?

下面是幾種常見的說法:

“它是GIL(全局解釋器鎖)”

“這是由於它是解釋的,而非編譯”

“這是由於它是一種動態類型語言”

那麼,到底上述哪個原因對性能帶來的影響最大?

“它是GIL”

現代計算機搭載擁有多個內核的CPU,有時搭載多個處理器。為了利用所有這些額外的處理能力,操作系統定義了一種名為線程的低級結構:一個進程(比如Chrome瀏覽器)可能生成多個線程,並擁有針對內部系統的指令。這樣一來,如果某個進程特別耗費CPU資源,該負載可以在諸多核心之間分擔,這實際上讓大多數應用程序更快地完成任務。

我在寫這篇文章時,我的Chrome瀏覽器有44個線程開著。請記住這點:線程的結構和API在基於POSIX的操作系統(比如Mac OS和Linux)與Windows OS之間是不同的。操作系統還處理線程的調度。

如果你之前沒有從事過多線程編程,需要儘快熟悉的一個概念就是鎖(lock)。與單線程進程不同,當你需要確保改變內存中的變量時,多個線程並不同時試圖訪問/改變同樣的內存地址。

CPython創建變量時,它會分配內存,然後計算該變量的引用有多少,這個概念名為引用計數(reference counting)。如果引用數為0,那麼它從系統釋放這部分內存。這就是為什麼在某個代碼段(比如for循環的範圍)內創建一個“臨時”變量不會搞砸應用程序的內存消耗。

當變量在多個線程內共享時,就出現了這個難題:CPython如何鎖定引用計數。有一個“全局解釋器鎖”,它小心地控制線程執行。解釋器一次只能執行一個操作,無論它有多少線程。

這對Python應用程序的性能來說意味著什麼?

如果你有單線程、單個解釋器的應用程序,這對速度不會有影響。刪除GIL根本不會影響你代碼的性能。

如果你想通過使用線程機制在單個解釋器(Python進程)內實現併發功能,而且線程是IO密集型(比如網絡IO或磁盤IO),你會看到GIL爭奪的後果。

如果你有Web應用程序(比如Django),又在使用WSGI,那麼針對Web應用程序的每個請求都是一個單獨的Python解釋器,所以每個請求只有一個鎖。由於Python解釋器啟動緩慢,一些WSGI實現擁有“守護進程模式”,這可以讓一個或多個Python進程為你保持活躍狀態。

其他Python運行時環境怎麼樣?

PyPy有一個GIL,它通常比CPython快3倍。

Jython之所以沒有GIL,是由於Jython中的Python線程由Java線程表示,受益於JVM內存管理系統。

JavaScript如何執行此任務?

好吧,首先所有Javascript引擎都使用標記-清除(mark-and-sweep)垃圾收集機制。如上所述,GIL的主要需求是CPython的內存管理算法。

JavaScript沒有GIL,但它也是單線程的,所以它不需要內存管理算法。JavaScript的事件循環和承諾回調(Promise/Callback)模式是實現異步編程以代替併發的方法。Python與asyncio事件循環有相似之處。

“這是由於它一種解釋語言”

我常聽到這個觀點,但覺得這過於簡化了CPython的實際工作方式。如果你在終端上編寫了python myscript.py,那麼CPython會啟動讀取、分析、解析、編譯、解釋和執行代碼的一長串操作。

如果你對這個過程的機理頗感興趣,我之前寫過一篇文章:《6分鐘內修改Python語言》(https://hackernoon.com/modifying-the-python-language-in-7-minutes-b94b0a99ce14)。

這個過程的一個重要節點是創建.pyc文件;在編譯階段,字節碼序列寫入到Python 3中__pycache__/裡面的一個文件或Python 2中的同一個目錄。這不僅適用於你的腳本,還適用於導入的所有代碼,包括第三方模塊。

所以在大部分時間(除非你編寫的是隻運行一次的代碼?),Python解釋字節碼,並在本地執行。相比之下Java和C#.NET:

Java編譯成一種“中間語言”,Java虛擬機讀取字節碼,並即時編譯成機器碼。.NET CIL也一樣,.NET公共語言運行時環境(CLR)使用即時編譯,將編譯後代碼編譯成機器碼。

那麼,既然都使用虛擬機和某種字節碼,為什麼Python在基準測試中比Java和C#都要慢得多呢?首先,.NET和Java是JIT編譯型的。

JIT或即時編譯需要一種中間語言,以便將代碼拆分成塊(或幀)。提前(AOT)編譯器旨在確保CPU在任何交互發生之前能理解每一行代碼。

JIT本身不會使執行變得更快,因為它仍然執行相同的字節碼序列。然而,JIT讓代碼在運行時能夠加以優化。一個好的JIT優化器會看到應用程序的哪些部分在頻繁執行,這些代碼稱之為“熱點代碼”(hot spot)。然後,它會對這些代碼進行優化,其辦法是把它們換成更高效的版本。

這就意味著當你的應用程序一次又一次地執行相同的操作時,運行速度可以顯著加快。另外記住一點:Java和C#是強類型語言,因此優化器可以對代碼做出多得多的假設。

PyPy有JIT,如上所述,其速度比CPython快得多。想要深入瞭解Python,獲取Python學習資料的小夥伴可以加QQ:2915541363

那麼,CPython為什麼不使用JIT呢?

JIT存在幾個缺點:缺點之一是啟動時間。CPython的啟動時間已經比較慢了,PyPy的啟動時間比CPython還要慢2倍至3倍。眾所周知,Java虛擬機的啟動速度很慢。.NET CLR通過系統開啟時啟動解決了這個問題,但CLR的開發人員還開發了操作系統,CLR在它上面運行。

如果你有一個Python進程長時間運行,代碼因含有“熱點代碼”而可以優化,那麼JIT大有意義。

然而,CPython是一種通用實現。所以,如果你在使用Python開發命令行應用程序,每次調用CLI都得等待JIT啟動會慢得要命。

CPython不得不試圖滿足儘可能多的用例(use case)。之前有人試過將JIT插入到CPython中,但這個項目基本上擱淺了。

如果你想獲得JIT的好處,又有適合它的工作負載,不妨使用PyPy。

“這是由於它是一種動態類型語言”

在“靜態類型”語言中,你在聲明變量時必須指定變量的類型。這樣的語言包括C、C++、Java、C#和Go。

在動態類型語言中,仍然存在類型這個概念,但變量的類型是動態的。

在這個示例中,Python創建了一個有相同名稱、str類型的第二個變量,並釋放為a的第一個實例創建的內存。

靜態類型語言不是為了給你添堵而設計的,它們是兼顧CPU的運行方式設計的。如果一切最終需要等同於簡單的二進制操作,你就得將對象和類型轉換成低級數據結構。

Python為你這麼做這項工作,你永遠看不到,也不需要操心。

不必聲明類型不是導致Python速度慢的原因,Python語言的設計使你能夠讓幾乎一切都是動態的。你可以通過猴子補丁(monkey-patch),加入對運行時聲明的值進行低級系統調用的代碼。幾乎一切都有可能。

正是這種設計使得優化Python異常困難。

為了說明我的觀點,我將使用可在Mac OS中使用的一種名為Dtrace的系統調用跟蹤工具。CPython發行版並未內置DTrace,所以你得重新編譯CPython。我使用3.6.6進行演示。

現在python.exe將在整個代碼中使用Dtrace跟蹤器。保羅•羅斯(Paul Ross)寫了一篇關於Dtrace的雜談(https://github.com/paulross/dtrace-py#the-lightning-talk)。你可以下載Python的DTrace啟動文件(https://github.com/paulross/dtrace-py/tree/master/toolkit)來測量函數調用、執行時間、CPU時間、系統調用和各種好玩的指標。比如

py_callflow跟蹤器顯示你應用程序中的所有函數調用。

那麼,Python的動態類型會讓它變慢嗎?

比較和轉換類型的開銷很大,每次讀取、寫入或引用一個變量,都要檢查類型。

很難優化一種極具動態性的語言。Python的許多替代語言之所以快得多,原因在於它們為了性能在靈活性方面作出了犧牲。

Python 結合了C-Static類型和Python來優化類型已知的代碼,可以將性能提升84倍。

結論

Python之所以速度慢,主要是由於動態性和多功能性。它可用作解決各種問題的工具,Python有更優化、速度更快的幾個替代方案。

然而,有一些方法可以優化你的Python應用程序,比如通過充分利用異步、深入瞭解分析工具以及考慮使用多個解釋器。

對於啟動時間不重要、代碼會受益於JIT的應用程序來說,不妨考慮PyPy。

對於性能至關重要,又有更多靜態類型變量的部分代碼而言,不妨考慮使用Python。


分享到:


相關文章: