為什麼Python這麼慢?

  • 2019 年 11 月 25 日
  • 筆記

Python越來越受歡迎。它被用於DevOps、數據科學、Web開發和安全。

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

就速度而言,Java與C或c++或c#或Python相比如何?

答案在很大程度上取決於您正在運行的應用程式的類型。沒有一個基準測試是完美的,但是電腦語言基準測試遊戲是一個很好的起點。

十多年來,我一直在參考電腦語言基準測試遊戲;與其他語言如Java、c#、Go、JavaScript、c++相比,Python是最慢的語言之一。這包括JIT (c#, Java)和AOT (C, c++)編譯器,以及解釋語言,如JavaScript。

注:當我說「Python」時,我指的是該語言的參考實現CPython。Python是一門語言,有語法等規範。但是落實到具體實現上,就不一樣了。用C實現的叫CPython,也是目前的參考實現。即最新的語言特性都是在這個上面先實現,Linux,OS X等自帶的也是這個版本。用.NET實現的叫IronPython,Java的叫Jython,用Python實現的叫PyPy

我想回答這個問題:當Python比另一種語言慢2 – 10倍完成一個可比較的應用程式時,為什麼它慢,我們不能使它更快?

以下是最熱門的理論:

  • 它是GIL(全局解釋器鎖)"
  • 因為它是解釋過的而不是編譯過的
  • 因為它是動態類型語言

這些原因中哪一個對性能影響最大?

我們逐個分析

1. 它是GIL(全局解釋器鎖)

現代電腦的CPU是多核的,有時也有多個處理器。為了利用所有這些額外的處理能力,作業系統定義了一個稱為執行緒的底層結構,其中一個進程(如Chrome瀏覽器)可以衍生多個執行緒,並在內部為系統提供指令。通過這種方式,如果一個進程是cpu密集型的,那麼可以跨內核共享負載,從而有效地使大多數應用程式更快地完成任務。

如果您以前沒有做過多執行緒編程,那麼您需要快速熟悉鎖的概念。與單執行緒進程不同,您需要確保在更改記憶體中的變數時,多個執行緒不會嘗試同時訪問/更改相同的記憶體地址。

當CPython創建變數時,它分配記憶體,然後計算有多少對該變數的引用存在,這是一個稱為引用計數的概念。如果引用的數量為0,那麼它將從系統中釋放那塊記憶體。這就是為什麼在for循環的範圍內創建「臨時」變數不會增加應用程式的記憶體消耗。

當變數在多個執行緒中共享時,挑戰就變成了CPython如何鎖定引用計數。有一個「全局解釋器鎖」,它小心地控制執行緒的執行。解釋器一次只能執行一個操作,不管它有多少執行緒

那麼其他Python runtimes呢?

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

Jython沒有GIL,因為Jython中的Python執行緒由Java執行緒表示,並且受益於JVM記憶體管理系統。

JavaScript是如何做到這一點的?

首先,所有Javascript引擎都使用標記-清除垃圾收集。如前所述,GIL的主要需求是CPython的記憶體管理演算法。

JavaScript沒有GIL,但它也是單執行緒的,所以不需要GIL。JavaScript的事件循環和承諾/回調模式是實現非同步編程而不是並發的方式。Python對非同步事件循環也有類似的處理。

2. 因為這是一種解釋語言

我經常聽到這種說法,我發現這是對CPython實際工作方式的一種粗略簡化。如果您在終端上編寫了python myscript.py,那麼CPython將開始一長串的讀取、詞法分析、解析、編譯、解釋和執行這些程式碼

在這個過程中很重要的一點是創建一個.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優化器會看到應用程式的哪些部分被頻繁地執行,稱之為「熱點」。然後,它將對這些程式碼進行優化,用更高效的版本替換它們。

這意味著當您的應用程式一次又一次地做同樣的事情時,它可以顯著地更快。另外,請記住Java和c#是強類型語言,因此優化器可以對程式碼進行更多的假設。

PyPy有一個JIT,正如前一節所提到的,它比CPython要快得多。

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

jit也有缺點:其中之一就是啟動時間。CPython的啟動時間已經比較慢了,PyPy比CPython慢2 – 3倍。眾所周知,Java虛擬機的啟動速度很慢。net CLR通過在系統啟動時啟動來解決這個問題,但是CLR的開發人員還開發運行CLR的作業系統。

如果您有一個運行了很長時間的Python進程,其中的程式碼可以進行優化,因為它包含「熱點」,那麼JIT就很有意義。

然而,CPython是一種通用實現。因此,如果您正在使用Python開發命令行應用程式,那麼每次調用CLI時都必須等待JIT啟動,這將是非常慢的。

CPython必須嘗試並服務儘可能多的用例。在CPython中插入JIT是有可能的,但是這個項目在很大程度上已經停止了。如果您希望獲得JIT的好處,並且有適合它的工作負載,那麼可以使用PyPy。

3. 因為它是動態類型語言

在「靜態類型」語言中,必須在聲明變數時指定變數的類型。包括C, c++, Java, c#, Go。在動態類型語言中,仍然有類型的概念,但是變數的類型是動態的。

a = 1  a = "foo"

在這個例子中,Python創建了第二個具有相同名稱和str類型的變數,並釋放為a的第一個實例創建的記憶體

靜態類型語言的設計並不是為了讓您的工作變得困難,而是因為CPU的操作方式。如果最終需要將所有操作都等同於簡單的二進位操作,則必須將對象和類型轉換為低級數據結構。

Python為您做了這些,您只是從來沒有見過它,也不需要關心它。

不需要聲明類型並不是使Python變慢的原因,Python語言的設計使您能夠使幾乎任何東西都是動態的。您可以在運行時替換對象上的方法,您可以在運行時對低級系統調用的值進行monkey-patch。幾乎一切皆有可能。

正是這種設計使得優化Python變得非常困難

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

  • 比較和轉換類型的成本很高,每次讀取、寫入或引用某個變數時,都要檢查該類型
  • 很難優化一門如此動態的語言。Python的許多替代品之所以如此之快,是因為它們在性能的名義下對靈活性做出了妥協
  • 看看Cython,它結合了C-Static類型和Python來優化已知類型的程式碼,可以提供84x的性能改進。

結論

Python的主要缺點是它的動態性和通用性。它可以作為解決各種問題的工具,在這些問題中,可能有更優化、更快的替代方案。

但是,可以通過利用非同步、理解分析工具和考慮使用多解釋器來優化Python應用程式。

對於啟動時間不重要且程式碼有利於JIT的應用程式,可以考慮使用PyPy。

對於您的程式碼中性能非常重要並且有更多靜態類型變數的部分,可以考慮使用Cython。

原文傳送門:

https://medium.com/hackernoon/why-is-python-so-slow-e5074b6fe55b