架構之路(二):效能

自由飛發表於2015-09-07

 

我們在上一篇部落格中設定了架構的目標,只有一個,就是可維護性。完全沒有提效能,這是故意的。

 

似乎程式設計師都是急性子,或許是被windows冗長的開機時間折磨夠了,有可能是因為提升效能的效果是最顯而易見的……總之,我發現,絕大部分程式設計師對效能的關注和熱情是無與倫比的!

  • C#剛剛推出的時候,就有人搖頭晃腦的說,“嗯,自動垃圾回收,效能不行吧?”
  • DataSet橫空出世,馬上有很多人寫程式碼,在DataSet裡插入幾百萬條資料,證明DataSet的效能問題
  • Linq當然更要被罵了,尼瑪用反射?反射是什麼,同學們知道麼?效能大老虎呀!更不用說那些自動生成的sql了,有我手寫的高效麼?
  • ……

所以直到今天,我仍然看到很多程式設計師無怨無悔的用儲存過程來構建他們的系統,一個儲存過程可以有幾千行!然後,他們很無辜的問,“業務層有什麼用?究竟能幹些什麼呢?”

 

在帶團隊的時候,我最怕講的就是效能有關的問題。你要是不談效能呢,那程式碼有時候真心看不下去;你要是強調效能呢,不知道他會給你整出什麼么蛾子出來。其實這就是一個“度”的掌握,所以非常難以用語言予以表示清楚。所以無數次挫敗之後,我只好咬牙切齒的說,“你的程式碼,只有一個評判標準,可維護性。效能的問題先不管!”這個答案似乎並不能服眾——尤其是對有上進心的程式設計師而言。

 

所以,我先專篇講效能,希望能幫助大家更清楚的認識這個問題。

 

一、效能不是不重要,而是他沒有可維護性重要。要理解這一點,首先要理解可維護性的重要(請再讀上一篇我花數週找bug的段子);然後要明白:解決效能問題,我們可以有很多程式碼以外行之有效的方法,而可維護性基本上就只能靠程式碼了;最後,還是要牢記:沒有犧牲,就沒有勝利!

二、所以,在絕大多數情況下,當效能和可維護性相沖突的時候,效能讓位於可維護性。我們採用其他辦法來彌補程式碼效能不夠高的問題。

 

空洞的說教沒有意義。我們還是舉例來說明吧!

 

破壞可讀性

 

前段時間我review程式碼的時候發現,這個程式設計師用Linq之後老是用First()而不是Single(),我就奇怪了,按業務邏輯,返回的值就應該是一個,難道可能會是多個,多個應報異常,不應該取First()就完事了呀?想了一會兒,問這個程式設計師,他的回答讓我瞬間一種無力感,“First()效能更高呀!”以下為對話實錄:

 

“你怎麼知道First()效能更高呢?”我問。

“First()嘛,取了第一個合格的值就返回,就不會繼續查下去了;Single()的話,就會一直查,查出所有資料,然後再取其中的一個。”

“你確定?你知道有一種東西叫做索引不?”

“啊?……”

然後我簡單的告訴他,索引是一種樹狀結構,可以讓查詢更快等等。

“但我還是覺得應該用First()”,他想了一會兒,還是很堅定。

“為什麼?”,我不明白了。

“就算有索引加快了查詢速度,但用First()在加快了速度上更快呀!更快總是沒錯的吧?”

“……”,我真不知道該怎麼說了,最後突然靈光一閃,“好吧,那你說說,微軟為什麼要搞一個Single()方法出來呢?就為了搞出來誤導你們?讓用First()的產生優越感,嘲笑用Single()的?”

他陷入了沉思。

 

評論裡還在糾結Single()/First()的同學,請大聲的吼三遍:可讀性!可讀性!!可讀性!!!

發現同學們還在糾結這個細節。好吧,再解釋一下:

1、你怎麼知道資料庫用的就是MSSQL呢?你怎麼知道就是用的關聯式資料庫呢?NoSQL不行麼?所以,你怎麼就知道Single()/First()具體是怎麼執行的呢?比如我就要寫個Linq實現,把所有的資料全取出來,然後再在記憶體裡排序,最後取First呢?

2、這裡我們考慮可讀性,意思是:讀程式碼時,看到Single()就能瞬間知道coder的意思是取唯一的一個;看到First()就知道coder的意思是要取第一個。和效能沒關係,如果一定要糾纏效能,那好:你要確定唯一性,當然要做檢查(包括不唯一時拋異常),這個效能損失是應該的呀;你要取第一個,當然要進行排序,排序也會有效能損失呀!

 

我剛入行的時候,還很是收藏了幾篇文章,比如《高效能程式設計的十大準則》之類的,裡面的內容大致就是,“總是使用StringBuilder,不要使用‘+’;總是使用……,不要使用……”。這類文章下面總是有一堆人叫好,“不錯!”,“謝謝分享!”但慢慢的,我就對這些文章產生了懷疑(也應該感謝園子裡的老趙,csdn裡面的sp1234之類的大神);直到很後來,我才明白為什麼這種說法是膚淺的;而只有通過上面的對話,我才能清晰的把我的理解說出來。

 

所有這些犧牲效能的簡單封裝,都是有其目的的;而其中一個很重要的目的,就是為了提高可讀性。你為了效能,故意不使用這些現成的封裝,通常,喪失的就是可讀性。

 

想當然

 

繼續上面這個例子。最開始的時候,這個程式設計師關於效能的考慮其實是想當然的。這種想當然的情形很多,大致有這幾種:

  1. 自己的理解完全就是錯的
  2. 自己的理解不能算錯,但實際上底層已經對該問題做了優化
  3. 自己的理解沒錯,底層也沒優化

第1、2種比較好理解,第3種為什麼也說他“想當然”呢?因為沒有和硬體環境相契合。

 

最簡單的例子就是“快取”。比如面試的時候,問你一個問題,“快取能不能提高效能?”請注意,這是一個陷阱。答案應該是:“不一定”。幾乎所有的人都認為,快取可以迅速改善效能,是因為今天計算機的CPU和磁碟執行速度,遠跟不上記憶體的發展。但即使如此,無節制的快取,一樣可以拖垮整個系統。

 

類似的例子還有很多。你沾沾自喜,我節約了一次磁碟讀寫的時候,你同時增加了CPU的負荷;你優化了演算法,減少了CPU的運算,但其實增加了記憶體的壓力……天下沒有免費的午餐。同樣的程式碼,隨著資料的增加,硬體的改變,會呈現出截然不同的效能表現。

 

所以,開發過程中,很多的“優化”,其實只是你的想當然。與其這樣想當然的優化,不如在拿到效能測試結果之後再有的放矢的進行優化。這時候,又回到了我們之前說的,是不是程式碼的可讀性更重要?這樣你才能迅速的找到該優化的瓶頸啊!否則,一堆亂七八糟看都看不懂的程式碼,你怎麼去優化,你連該優化的點都找不到。

 

難以維護

 

另一個搞笑的例子是關於我自己的。創業家園專案裡有一個功能:顯示部落格正文的同時提供一個上一頁下一頁的連結。慣常的做法就是直接在資料庫裡查就是了,但我總覺得不對,這樣做兩次查詢有必要麼?能不能優化?於是我想到了一個“絕妙”的點子:為什麼不直接在部落格裡儲存上一篇和下一篇的Id呢?這樣我一次性資料往返就能取到所有資料了嘛!各位同學是不是覺得我這個主意很棒?

 

噩夢由此開始了。

 

首先,我們是想在釋出部落格的時候,設定他的上一篇和下一篇。但是,上一篇好設定,下一篇呢?還沒有啊!怎麼弄,就只好在部落格釋出的時候,設定他的前一篇,同時設定他前一篇的後一篇。

然後,我們新新增了一個功能,除了上一篇下一篇以外,還需要在當前部落格所在分類中的上一篇和下一篇。怎麼辦?再加欄位唄。所以,部落格裡就有了Previous, PreviousInCategory, Next, NextInCategory。這時候,就感覺到有點不妥,但還可以接受。

接著,出現了一個問題,上一篇下一篇部落格被刪除了,怎麼辦?這個過程,就相當於從一個雙向連結串列裡移出一個節點一樣麻煩。頭開始有點大了。

再接著,部落格除了釋出刪除以外,還有各種其他狀態,比如被遮蔽。而且被遮蔽之後,能否顯示和當前使用者又有關係。當前使用者是普通使用者,不能閱讀;當前使用者是作者自己,就能夠閱讀。怎麼辦?首先,遮蔽的時候,要設定上一篇下一篇;遮蔽取消的時候,還是要設定上一篇下一篇。然後,上一篇下一篇得根據當前使用者不同變化的這個問題,基本上就傻眼了……

 

最後流著淚把辛辛苦苦折騰了好久的程式碼全改回來,就通過資料庫查唄,多麼清晰簡潔的邏輯啊!效能問題?首先,這樣做造成了效能問題麼?然後,就算有問題,用一個快取能解決不?

 

合理浪費堆硬體

 

說了這麼多,不知道有沒有引起同學們的反思。可能大家還是過不去心裡那道坎:明明有一種效能更高的方法我們為什麼不用?

 

因為浪費唄!

 

什麼?你有沒有搞錯?我的程式碼,至少省了一塊記憶體條!那是你還沒從“窮學生”的角色裡轉換過來。你花一週的時間對程式碼進行了優化(就先不考慮你的優化帶來的維護成本增加了),為老闆省下了一塊記憶體條的錢。你以為老闆會拍著你的肩膀表揚你麼?老闆打不死你!

 

兄弟,賬不是你那樣算的。你是學生的時候,你的時間成本是0;但你進入工作崗位,每一天都是要發工資的。

 

通過程式碼來調高效能,是一種無奈——對硬體效能不夠的妥協(參考:80年代遊戲開發者的辛苦困境。這樣寫效能就高,但為什麼現在沒有誰再這麼寫程式碼了?)。否則,絕大多數情況下,堆硬體比優化程式碼的效果好得多,而且便宜得多。硬體的成本按摩爾定律往下降,我們程式設計師的工資也能按摩爾定律減麼?

 

明明window 10 比window 95更耗效能,為什麼今天沒人用window 95?為什麼VS 2013要10G的空間我們都還屁顛屁顛的趕緊裝上?為什麼現在大家都用C#,沒人用匯編?我們站在人類文明積累的今天,就應該理所當然的享受這一切成果。有打火機你不用,你要鑽木取火。如果你是因為要學貝爺荒野求生裝逼,可以理解;如果你說你是因為怕浪費天然氣,我……我……我怎麼說你呢?“給做打火機的一條活路,行不?”同樣的,程式設計師大神同學,你就當做好事,給下面寫底層做硬體的一條活路吧!你的程式碼都是010001000010000001010101……了,你讓其他人怎麼活啊?

 

最後,我突然想到的一個程式設計師為什麼對效能如此敏感瘋狂,對可維護性毫不在意的一個可能原因:

  • 效能很好理解,卡得要死和跑得飛快;可維護性很不好理解,至少得跑個兩三年才能體現,那時候,誰知道爺在哪裡偷著樂呢
  • 效能上不來,程式設計師只有羞愧的低著頭,都是我的錯;需求有變更,開口就罵,“哪個SB又要改……”;

大家覺得是不是這樣的?所以,願意把程式碼百鍊成鋼繞指柔的人少。想來,是一種莫名的悲哀和淒涼。

 

最後最後,有一些我能想到的名言警句供大家參詳:

  • 過早的優化是萬惡之源
  • 優化首先需要找到效能“瓶頸”。否則,任何人都可以隨手一指,“這段程式碼需要優化”。
  • 可讀性更強的程式碼總是更好優化
  • 硬體永遠比軟體便宜

 

忘了說我的專案了。目前主要集中在創業家園專案的開發上,正試圖從svn轉成git原始碼控制。不太懂Git,說起來都是淚,懂的同學幫幫忙吧!

相關文章