為什麼你寫的Python執行的那麼慢呢?

ourjs發表於2014-03-13

  大約在一年前,也就是2013年在Waza(地名),Alex Gaynor提到了一個很好的話題:為什麼用Python、Ruby和Javascript寫的程式總是執行的很慢呢?正如他強調的,關鍵就是現在出現了這個問題。換一句話說,儘管現在這種語言很慢,但不意味著沒有解決辦法,不意味著未來會一直這樣。

  當在網上問為什麼Python比C語言更慢,回答最多的就是Python中有動態型別。然而,動態型別確實會在效能方面有影響,但是這並不是主要原因。

  動態型別(像Python一樣的主要程式語言都一樣)使得編譯器很難優化效能。動態使得每次執行都可能很不同,編譯器難以優化。然而,正如Alex在談話中提到的,我們花費了數年的時間來研究究竟在執行時進行型別檢查的最好的辦法是什麼。但是沒什麼進展。

  在現實中,在C語言和Python在執行時的巨大的不同是由於資料結構和演算法的不同。有時程式設計師也沒有注意到這一點。

用Python寫不同的程式碼

  讓我們用一個Alex提到的例項來說明問題。一個Python程式設計師可能很喜歡用下面的例子表示一個平面上的點:

point = {'x': 0, 'y': 0}

  這種方法很易讀,容易編碼,形式很優雅。

  另一個方面,一個C語言程式設計師可能使用結構體來表示平面上的點:

struct Point {
   int x;
   int y;
};

  儘管這種方法也和Python能一樣的工作並且都是很優雅的,但這是完全不同的資料結構。這裡我們告訴了編譯器,我們有兩個欄位x和y。知道了這兩個欄位的型別,編譯器將分配一塊連續的記憶體來儲存這兩個資料。換一句話說,就像一個陣列一樣。任何時間,編譯器都知道給定的x和y在哪裡。我們可以很容易地訪問這些資料,就像是訪問某些常資料一樣。

  Python使用雜湊雜湊的方法來解決類似的問題。所以編譯器不能簡單地分配連續記憶體儲存x和y來處理這些問題。由於我們在其中任意的地方都可能出現這些鍵。如果我們想的話,我們也可能刪除這些鍵。編譯器必須要使用雜湊函式來對映到你可能讓他指向的任何儲存單元。不用說,這些函式增加了處理時間。儘管也許減緩的很小,但是足可以拖慢你的程式碼,尤其是這種情況如果很多的時候。

  如果就是想將Python翻譯成C語言的話,可能就像下面這樣:

std::hash_set point;
point[“x”] = x
point[“y”] = y

  看這個程式碼片段,好像就是語言的設計者他們自己故意盡力使雜湊表複雜,因此儘管是正確的,但沒有人使用。由於這個原因,寫C語言的人可能認為這是不可思議的,但為什麼在Python就是可以接受的呢?

  原因就是寫Python程式碼的人的“dictionaries are lightweight objects”這種心態。看下面的程式碼,這在Python中最接近C語言結構體:

class Point(object):
     x, y = None, None
     def __init__(self, x, y):
          self.x, self.y = x, y

         

  這對編譯器是有用的,就像是C語言的結構體。例如第二行,我們明確告訴編譯器但我們創造一個物件時我們總是至少需要兩個資料段,我們希望編譯器處理這個問題。

  不幸的是這種標準的Python被叫做CPthon,不能總被使用。在我的機器上,下面的程式碼要執行186毫秒:

def sum_(points):
    sum_x, sum_y = 0, 0
    for point in points:
        sum_x += point['x']
        sum_y += point['y']
    return sum_x, sum_y

  在我的機器上,用point.x代替point['x']會花費201毫秒。也就是說,會慢了8%。

  在CPthon中,point.x通常就是被處理成dict(point)['x']。這意味著帶著點的class仍然像以前一樣使用字典(dictionary)的方法查詢。這樣的話,就很容易看出為什麼directionary的方法被看為“輕量級的”。

  一些Python寫的程式碼就是為了效率而設計的,例如PyPy,能很快地執行。如果不使用Python而是使用PyPy,同樣的程式碼片段執行時間分別是21.6和3.75毫秒。這種方法相比CPython在JIT-capable編譯情況下結果都是令人滿意的。換一句話說,PyPy能正確地使用資料結構。

  我希望你再一次看這個最短時間3.75毫秒。這個數字表明我們能在一秒進行266000次運算,這些事來自Python的,其中有動態繫結,monkey-patching(在不改變原始碼的情況下擴充套件或修改動態語言執行時程式碼的方法)等。所有的這些,都是在編碼和實現中使用了更好的資料結構。下一次當你在用Python寫一行程式碼時,想一想你在使用什麼資料結構,顯示的還是隱式的,考慮一下是否有更好的辦法。這就是你用C語言寫程式時考慮的,不是嗎?

  最後,我願意相信這個文章是表明為什麼Python是一個有前途的語言的一個清楚的例子(或者是類似的語言)。這表明了標準的Python實現,這裡的CPython僅僅是作為一個參考,它從來就不是被設計用來更快地執行的。正如我們今天可以看到的,像PyPy一樣的演算法實現是可以優化你的程式碼到一個很好的長度。隨著語言的自然發展,這些優化是可能的。我們僅僅用Python程式設計過23年,那麼如果像C語言一樣有42年的發展,Python會是什麼樣子呢?

  1、有人也許會爭辯說collections.namedtuple()是更接近於C語言的結構體,這是對的,但我們不要過分將事情複雜化,我們的重點是有效。

  2、為了更清楚python是怎樣工作的,請參考python文件。

  原文 lukauskas.co.uk

相關文章