天天看點

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,自己多實踐。這兒是你的第一個程式:

import hello

但是如果速度真的重要呢?

上述論點的語氣可能會讓人覺得優化與速度一點也不重要。但事實是,很多時候運作時性能真的很重要。一個例子是,你有一個 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 的性能問題阻礙時,你不需要把你的整個代碼庫用另一種不同的語言來編寫。你隻需要用 cython 重寫幾個函數,幾乎就能得到你所需要的性能。這就是星戰前夜采取的政策。這是一個大型多玩家的電腦遊戲,在整個架構中使用 python 和 cython。它們通過優化 c/cython 中的瓶頸來實作遊戲級别的性能。如果這個政策對他們有用,那麼它應該對任何人都有幫助。或者,還有其他方法來優化你的 python。例如,pypy是一個 python 的 jit 實作,它通過使用 pypy 替掉 cpython(這是 python 的預設實作),為長時間運作的應用程式提供重要的運作時改進(如 web 伺服器)。

讓我們回顧一下要點:

優化你最貴的資源。那就是你,而不是計算機。

選擇一種語言/架構/架構來幫助你快速開發(比如 python)。不要僅僅因為某些技術的快而選擇它們。

當你遇到性能問題時,請找到瓶頸所在。

你的瓶頸很可能不是 cpu 或者 python 本身。

如果 python 成為你的瓶頸(你已經優化過你的算法),那麼可以轉向熱門的 cython 或者 c。

盡情享受可以快速做完事情的樂趣。