Python 是慢,但我無所謂
為犧牲效能追求生產率而吶喊
讓我從關於 Python 中的 asyncio 這個標準庫的討論中休息一會,談談我最近正在思考的一些東西:Python 的速度。對不瞭解我的人說明一下,我是一個 Python 的粉絲,而且我在我能想到的所有地方都積極地使用 Python。人們對 Python 最大的抱怨之一就是它的速度比較慢,有些人甚至拒絕嘗試使用 Python,因為它比其他語言速度慢。這裡說說為什麼我認為應該嘗試使用 Python,儘管它是有點慢。
速度不再重要
過去的情形是,程式需要花費很長的時間來執行,CPU 比較貴,記憶體也很貴。程式的執行時間是一個很重要的指標。計算機非常的昂貴,計算機執行所需要的電也是相當貴的。對這些資源進行優化是因為一個永恆的商業法則:
優化你最貴的資源。
在過去,最貴的資源是計算機的執行時間。這就是導致電腦科學致力於研究不同演算法的效率的原因。然而,這已經不再是正確的,因為現在矽晶片很便宜,確實很便宜。執行時間不再是你最貴的資源。公司最貴的資源現在是它的員工時間。或者換句話說,就是你。把事情做完比把它變快更加重要。實際上,這是相當的重要,我將把它再次放在這裡,彷彿它是一個引文一樣(給那些只是粗略瀏覽的人):
把事情做完比快速地做事更加重要。
你可能會說:“我的公司在意速度,我開發一個 web 應用程式,那麼所有的響應時間必須少於 x 毫秒。”或者,“我們失去了客戶,因為他們認為我們的 app 執行太慢了。”我並不是想說速度一點也不重要,我只是想說速度不再是最重要的東西;它不再是你最貴的資源。
速度是唯一重要的東西
當你在程式設計的背景下說 速度 時,你通常是說效能,也就是 CPU 週期。當你的 CEO 在程式設計的背景下說 速度 時,他指的是業務速度,最重要的指標是產品上市的時間。基本上,你的產品/web 程式是多麼的快並不重要。它是用什麼語言寫的也不重要。甚至它需要花費多少錢也不重要。在一天結束時,讓你的公司存活下來或者死去的唯一事物就是產品上市時間。我不只是說創業公司的想法 — 你開始賺錢需要花費多久,更多的是“從想法到客戶手中”的時間期限。企業能夠存活下來的唯一方法就是比你的競爭對手更快地創新。如果在你的產品上市之前,你的競爭對手已經提前上市了,那麼你想出了多少好的主意也將不再重要。你必須第一個上市,或者至少能跟上。一但你放慢了腳步,你就輸了。
企業能夠存活下來的唯一方法就是比你的競爭對手更快地創新。
一個微服務的案例
像 Amazon、Google 和 Netflix 這樣的公司明白快速前進的重要性。他們建立了一個業務系統,可以使用這個系統迅速地前進和快速的創新。微服務是針對他們的問題的解決方案。這篇文章不談你是否應該使用微服務,但是至少要理解為什麼 Amazon 和 Google 認為他們應該使用微服務。
微服務本來就很慢。微服務的主要概念是用網路呼叫來打破邊界。這意味著你正在把使用的函式呼叫(幾個 cpu 週期)轉變為一個網路呼叫。沒有什麼比這更影響效能了。和 CPU 相比較,網路呼叫真的很慢。但是這些大公司仍然選擇使用微服務。我所知道的架構裡面沒有比微服務還要慢的了。微服務最大的弊端就是它的效能,但是最大的長處就是上市的時間。通過在較小的專案和程式碼庫上建立團隊,一個公司能夠以更快的速度進行迭代和創新。這恰恰表明了,非常大的公司也很在意上市時間,而不僅僅只是隻有創業公司。
CPU 不是你的瓶頸
如果你在寫一個網路應用程式,如 web 伺服器,很有可能的情況會是,CPU 時間並不是你的程式的瓶頸。當你的 web 伺服器處理一個請求時,可能會進行幾次網路呼叫,例如到資料庫,或者像 Redis 這樣的快取伺服器。雖然這些服務本身可能比較快速,但是對它們的網路呼叫卻很慢。這裡有一篇很好的關於特定操作的速度差異的部落格文章。在這篇文章裡,作者把 CPU 週期時間縮放到更容易理解的人類時間。如果一個單獨的 CPU 週期等同於 1 秒,那麼一個從 California 到 New York 的網路呼叫將相當於 4 年。那就說明了網路呼叫是多少的慢。按一些粗略估計,我們可以假設在同一資料中心內的普通網路呼叫大約需要 3 毫秒。這相當於我們“人類比例” 3 個月。現在假設你的程式是高 CPU 密集型,這需要 100000 個 CPU 週期來對單一呼叫進行響應。這相當於剛剛超過 1 天。現在讓我們假設你使用的是一種要慢 5 倍的語言,這將需要大約 5 天。很好,將那與我們 3 個月的網路呼叫時間相比,4 天的差異就顯得並不是很重要了。如果有人為了一個包裹不得不至少等待 3 個月,我不認為額外的 4 天對他們來說真的很重要。
上面所說的終極意思是,儘管 Python 速度慢,但是這並不重要。語言的速度(或者 CPU 時間)幾乎從來不是問題。實際上谷歌曾經就這一概念做過一個研究,並且他們就此發表過一篇論文。那篇論文論述了設計高吞吐量的系統。在結論裡,他們說到:
在高吞吐量的環境中使用解釋性語言似乎是矛盾的,但是我們已經發現 CPU 時間幾乎不是限制因素;語言的表達性是指,大多數程式是源程式,同時它們的大多數時間花費在 I/O 讀寫和本機的執行時程式碼上。而且,解釋性語言無論是在語言層面的輕鬆實驗還是在允許我們在很多機器上探索分佈計算的方法都是很有幫助的,
再次強調:
CPU 時間幾乎不是限制因素。
如果 CPU 時間是一個問題怎麼辦?
你可能會說,“前面說的情況真是太好了,但是我們確實有過一些問題,這些問題中 CPU 成為了我們的瓶頸,並造成了我們的 web 應用的速度十分緩慢”,或者“在伺服器上 X 語言比 Y 語言需要更少的硬體資源來執行。”這些都可能是對的。關於 web 伺服器有這樣的美妙的事情:你可以幾乎無限地負載均衡它們。換句話說,可以在 web 伺服器上投入更多的硬體。當然,Python 可能會比其他語言要求更好的硬體資源,比如 c 語言。只是把硬體投入在 CPU 問題上。相比於你的時間,硬體就顯得非常的便宜了。如果你在一年內節省了兩週的生產力時間,那將遠遠多於所增加的硬體開銷的回報。
那麼,Python 更快一些嗎?
這一篇文章裡面,我一直在談論最重要的是開發時間。所以問題依然存在:當就開發時間而言,Python 要比其他語言更快嗎?按常規慣例來看,我、google 還有其他幾個人可以告訴你 Python 是多麼的高效。它為你抽象出很多東西,幫助你關注那些你真正應該編寫程式碼的地方,而不會被困在瑣碎事情的雜草裡,比如你是否應該使用一個向量或者一個數組。但你可能不喜歡只是聽別人說的這些話,所以讓我們來看一些更多的經驗資料。
在大多數情況下,關於 python 是否是更高效語言的爭論可以歸結為指令碼語言(或動態語言)與靜態型別語言兩者的爭論。我認為人們普遍接受的是靜態型別語言的生產力較低,但是,這有一篇優秀的論文解釋了為什麼不是這樣。就 Python 而言,這裡有一項研究,它調查了不同語言編寫字串處理的程式碼所需要花費的時間,供參考。
在上述研究中,Python 的效率比 Java 高出 2 倍。有一些其他研究也顯示相似的東西。 Rosetta Code 對程式語言的差異進行了深入的研究。在論文中,他們把 python 與其他指令碼語言/解釋性語言相比較,得出結論:
Python 更簡潔,即使與函式式語言相比較(平均要短 1.2 到 1.6 倍)
普遍的趨勢似乎是 Python 中的程式碼行總是更少。程式碼行聽起來可能像一個可怕的指標,但是包括上面已經提到的兩項研究在內的多項研究表明,每種語言中每行程式碼所需要花費的時間大約是一樣的。因此,限制程式碼行數就可以提高生產效率。甚至 codinghorror(一名 C# 程式設計師)本人寫了一篇關於 Python 是如何更有效率的文章。
我認為說 Python 比其他的很多語言更加的有效率是公正的。這主要是由於 Python 有大量的自帶以及第三方庫。這裡是一篇討論 Python 和其他語言間的差異的簡單的文章。如果你不知道為何 Python 是如此的小巧和高效,我邀請你藉此機會學習一點 python,自己多實踐。這兒是你的第一個程式:
但是如果速度真的重要呢?
上述論點的語氣可能會讓人覺得優化與速度一點也不重要。但事實是,很多時候執行時效能真的很重要。一個例子是,你有一個 web 應用程式,其中有一個特定的端點需要用很長的時間來響應。你知道這個程式需要多快,並且知道程式需要改進多少。
在我們的例子中,發生了兩件事:
- 我們注意到有一個端點執行緩慢。
- 我們承認它是緩慢,因為我們有一個可以衡量是否足夠快的標準,而它沒達到那個標準。
我們不必在應用程式中微調優化所有內容,只需要讓其中每一個都“足夠快”。如果一個端點花費了幾秒鐘來響應,你的使用者可能會注意到,但是,他們並不會注意到你將響應時間由 35 毫秒降低到 25 毫秒。“足夠好”就是你需要做到的所有事情。免責宣告: 我應該說有一些應用程式,如實時投標程式,確實需要細微優化,每一毫秒都相當重要。但那只是例外,而不是規則。
為了明白如何對端點進行優化,你的第一步將是配置程式碼,並嘗試找出瓶頸在哪。畢竟:
任何除了瓶頸之外的改進都是錯覺。Any improvements made anywhere besides the bottleneck are an illusion. — Gene Kim
如果你的優化沒有觸及到瓶頸,你只是浪費你的時間,並沒有解決實際問題。在你優化瓶頸之前,你不會得到任何重要的改進。如果你在不知道瓶頸是什麼前就嘗試優化,那麼你最終只會在部分程式碼中玩耍。在測量和確定瓶頸之前優化程式碼被稱為“過早優化”。人們常提及 Donald Knuth 說的話,但他聲稱這句話實際上是他從別人那裡聽來的:
過早優化是萬惡之源Premature optimization is the root of all evil。
在談到維護程式碼庫時,來自 Donald Knuth 的更完整的引文是:
在 97% 的時間裡,我們應該忘記微不足道的效率:過早的優化是萬惡之源。然而在關 鍵的 3%,我們不應該錯過優化的機會。 —— Donald Knuth
換句話說,他所說的是,在大多數時間你應該忘記對你的程式碼進行優化。它幾乎總是足夠好。在不是足夠好的情況下,我們通常只需要觸及 3% 的程式碼路徑。比如因為你使用了 if 語句而不是函式,你的端點快了幾納秒,但這並不會使你贏得任何獎項。
過早的優化包括呼叫某些更快的函式,或者甚至使用特定的資料結構,因為它通常更快。電腦科學認為,如果一個方法或者演算法與另一個具有相同的漸近增長(或稱為 Big-O),那麼它們是等價的,即使在實踐中要慢兩倍。計算機是如此之快,演算法隨著資料/使用增加而造成的計算增長遠遠超過實際速度本身。換句話說,如果你有兩個 O(log n) 的函式,但是一個要慢兩倍,這實際上並不重要。隨著資料規模的增大,它們都以同樣的速度“慢下來”。這就是過早優化是萬惡之源的原因;它浪費了我們的時間,幾乎從來沒有真正有助於我們的效能改進。
就 Big-O 而言,你可以認為對你的程式而言,所有的語言都是 O(n),其中 n 是程式碼或者指令的行數。對於同樣的指令,它們以同樣的速率增長。對於漸進增長,一種語言的速度快慢並不重要,所有語言都是相同的。在這個邏輯下,你可以說,為你的應用程式選擇一種語言僅僅是因為它的“快速”是過早優化的最終形式。你選擇某些預期快速的東西,卻沒有測量,也不理解瓶頸將在哪裡。
為您的應用選擇語言只是因為它的“快速”,是過早優化的最終形式。
優化 Python
我最喜歡 Python 的一點是,它可以讓你一次優化一點點程式碼。假設你有一個 Python 的方法,你發現它是你的瓶頸。你對它優化過幾次,可能遵循這裡和那裡的一些指導,現在,你很肯定 Python 本身就是你的瓶頸。Python 有呼叫 C 程式碼的能力,這意味著,你可以用 C 重寫這個方法來減少效能問題。你可以一次重寫一個這樣的方法。這個過程允許你用任何可以編譯為 C 相容彙編程式的語言,編寫良好優化後的瓶頸方法。這讓你能夠在大多數時間使用 Python 編寫,只在必要的時候都才用較低階的語言來寫程式碼。
有一種叫做 Cython 的程式語言,它是 Python 的超集。它幾乎是 Python 和 C 的合併,是一種漸進型別的語言。任何 Python 程式碼都是有效的 Cython 程式碼,Cython 程式碼可以編譯成 C 程式碼。使用 Cython,你可以編寫一個模組或者一個方法,並逐漸進步到越來越多的 C 型別和效能。你可以將 C 型別和 Python 的鴨子型別混在一起。使用 Cython,你可以獲得混合後的完美組合,只在瓶頸處進行優化,同時在其他所有地方不失去 Python 的美麗。
星戰前夜的一幅截圖:這是用 Python 編寫的 space MMO 遊戲。
當您最終遇到 Python 的效能問題阻礙時,你不需要把你的整個程式碼庫用另一種不同的語言來編寫。你只需要用 Cython 重寫幾個函式,幾乎就能得到你所需要的效能。這就是星戰前夜採取的策略。這是一個大型多玩家的電腦遊戲,在整個架構中使用 Python 和 Cython。它們通過優化 C/Cython 中的瓶頸來實現遊戲級別的效能。如果這個策略對他們有用,那麼它應該對任何人都有幫助。或者,還有其他方法來優化你的 Python。例如,PyPy 是一個 Python 的 JIT 實現,它通過使用 PyPy 替掉 CPython(這是 Python 的預設實現),為長時間執行的應用程式提供重要的執行時改進(如 web 伺服器)。
讓我們回顧一下要點:
- 優化你最貴的資源。那就是你,而不是計算機。
- 選擇一種語言/框架/架構來幫助你快速開發(比如 Python)。不要僅僅因為某些技術的快而選擇它們。
- 當你遇到效能問題時,請找到瓶頸所在。
- 你的瓶頸很可能不是 CPU 或者 Python 本身。
- 如果 Python 成為你的瓶頸(你已經優化過你的演算法),那麼可以轉向熱門的 Cython 或者 C。
- 盡情享受可以快速做完事情的樂趣。