在不犧牲可讀性的情況下編寫高效的程式碼

已完成

平衡程式碼效率和可讀性是軟體開發人員的關鍵技能。 雖然效能至關重要,但不應以犧牲程式碼清晰度和可維護性為代價。

清晰為先,需要時優化

軟體工程的指導原則是首先使程式碼正確、清晰地工作,然後在必要時進行最佳化。 在知道什麼真正需要它之前,不要開始優化。

當您嘗試最佳化特定程式碼區段,而沒有證據證明程式碼速度緩慢時,您可能會面臨下列問題:

  • 使程式碼更難理解和維護: 複雜的最佳化(尤其是微優化)可能會引入複雜的邏輯、晦澀難懂的駭客攻擊或特殊情況的程式碼。 未來的維護者可能很難理解它,或者更糟的是,在修改它時引入錯誤。

  • 浪費時間: 您可能會花費數小時來調整對整體效能影響可以忽略不計的東西,而忽略了其他地方更大的問題。

  • 降低可靠性或靈活性: 有時,極端的效能調整會移除抽象層或錯誤檢查。 例如,在 C# (不安全程式碼) 中使用指標算術來提高速度可能會獲得一點效能,但會面臨很高的安全風險和可移植性損失。 這種類型的變更在商業應用程式中很難證明其合理性。

從明確的解決方案開始。 請記住,開發人員閱讀程式碼的頻率高於撰寫程式碼的頻率。 強調可讀的命名、結構和簡單性。 撇開高階演算法選擇不談,許多微最佳化 (例如快取瑣碎的計算或節省幾個 CPU 週期) 不值得以清晰度作為代價。 現代編譯器和硬體擅長有效率地執行簡單的程式碼。

最佳化何時有意義?

編寫程式碼的初始版本後,識別任何需要最佳化的關鍵部分(「關鍵 3%」)。 以下是優化(即使它使程式碼有點複雜)是合理的跡象和場景:

  • 已確認的熱點: 分析顯示特定方法或迴圈會耗用大量百分比的執行時間。 範例:您分析並找出一個函式佔程式執行時間的 60%。 如果優化可以將該函數的時間縮短一半,那麼它就會產生顯著的整體勝利。

  • 明顯的演算法效率低下: 有時您 知道 ,從大 O 的角度來看,更簡單的方法效率會大大降低。 例如,使用雙巢狀迴圈來比較兩個大型清單的元素是 O(n*m);如果您改為針對一個清單使用雜湊集合,則可能可降至 O(n+m)。 如果nm都可以很大,那麼差異就會非常巨大。 在這種情況下,經驗豐富的開發人員可能會從一開始就實施更有效的方法——如果需求很明顯,那還不是「為時過早」。 至關重要的是,如果做得好(使用描述性方法名稱、註釋等),許多演算法改進 不會降低程式碼的可讀性

  • 重複操作: 如果一段程式碼偶爾運行,那麼小的低效率也沒問題。 但是,如果它每秒運行數千次(例如,在緊密迴圈或高頻服務呼叫中),您會更加仔細地檢查它。 例如,建構新物件可能沒問題,但在每秒循環中執行 100,000 次(當您可以重複使用物件時)可能需要進行更改。

  • 效能關鍵型網域: 在某些領域(如遊戲開發、即時系統或嵌入式系統),效能要求非常嚴格。 在這裡,開發人員經常從一開始就考慮效率,因為天真的方法可能無法滿足要求。 即便如此,他們仍然依賴已知的模式和最佳實踐,而不是不可預測的低階調整。

目標是在 數據支持或領域上下文需要時進行優化,即使如此,也要以可維護的方式進行。

可讀性與優化

在編寫易於閱讀的程式碼和高度最佳化的程式碼之間通常需要權衡。 然而,許多優化可以在不犧牲清晰度的情況下實現。 讓我們看幾個例子來說明可讀性和效率之間的平衡。

使用適當的資料結構

假設你有一個集合,需要反覆檢查集合是否包含某個值。 您有數個選項:

  • 可讀但效率較低: 每次遍歷List<T>以尋找值。 這種方法對每次檢查都有 O(n) 的複雜性,並且程式碼保持清晰和簡單(透過基本循環或使用 List.Contains,執行內部迭代)。

  • 高效且可讀: 使用 a HashSet<T> 或 a Dictionary<TKey, TValue> 進行查找,每次檢查給出 O(1) 個平均時間。 程式碼會多一點點 (您填入 HashSet 並使用其 Contains 方法),但它仍然很清楚。 事實上,使用 HashSet 甚至可能更具表現力:它告訴讀者“我們需要快速查找”。 在這種情況下,更有效的解決方案也是乾淨的。

  • 過度優化且可讀性較差: 人為的替代方案可能涉及低階位元操作或針對此特定資料集量身定制的自訂雜湊演算法。 這種方法可能會讓程式碼維護人員感到困惑,並且與標準 HashSet 相比只能提供最小的效能提升(如果有任何改進的話)。 除非分析顯示內建資料結構會造成效能瓶頸,而且自訂實作確實必要 (這種情況很少發生),否則您應該避免此策略。

迴圈擴充或手動內嵌

開發人員有時候會嘗試透過展開迴圈或手動內嵌程式碼來減少迴圈的額外負荷,以最佳化迴圈。 考慮一個處理陣列的循環:

  • 可讀的: 編寫一個處理 100 個元素的陣列的循環。 代碼簡潔明了。 編譯器可以很好地優化它,任何現代 CPU 都可以輕鬆處理 100 次迭代。

  • 過度優化: 透過撰寫 100 次相同的語句來「展開」迴圈,以避免迴圈處理的額外負荷。 這種方法可能會節省一些循環控制的 CPU 週期,但您的程式碼現在是 100 行重複的語句 - 顯然不值得。 如果你改成 101 個元素,維護起來將是一場噩夢!

  • 重要的時候: 對於性能敏感的內迴圈(常見於高效能計算或算法庫中),開發人員偶爾會採用部分迴圈展開來進行最佳化。 不過,編譯器通常會自動或透過其他最佳化機制來處理此案例,而不需要在應用程式程式碼中手動實作。 作為應用程式開發人員,依賴編譯器的最佳化能力,維護簡單、清晰的程式碼。

C# 中的字串串連

字串串聯是效能和可讀性可能衝突的常見場景。 當重複執行字串串連時,方法的選擇會顯著影響效能。

  • 簡單方法:在循環中使用string += string。 範例:透過在迴圈中附加行來建立長 SQL 查詢或 CSV。 此技術易於閱讀,但在 .NET 中,每個 += 字串都會建立一個新字串(因為字串是不可變的)。 如果附加 1,000 次,就會建立許多中間字串物件,這段程式碼在時間和記憶體上都很低效。

  • 更好的方法:StringBuilder 進行多次串連。 本課程是針對該場景設計的;它會在緩衝區中建置字串,並在結尾產生一個最後的字串。 程式碼稍微冗長一些(您必須呼叫 Append 而不是 +=),但仍然很容易理解。 它清楚地表明“我們正在有效地構建一個字串”。事實上,.NET 最佳實踐指南建議在迴圈內使用 StringBuilder 進行連結。 用於 StringBuilder 重複的字串串連既更具可讀性(對於經驗豐富的開發人員)又更具效能。

此範例顯示,有時小的變更 (使用不同的 API) 會產生大幅的效能提升,但對可讀性的影響最小。 最初的方法可能適用於小字串,但如果您遇到大輸入,效能差異就會很大。

快取結果

快取是一種常見的最佳化技術,可以透過儲存資源密集的函數呼叫的結果並在再次出現相同的輸入時重複使用它們來提高效能。

  • 不緩存: 想像一個函數 GetExchangeRate(currency) 透過 HTTP 呼叫取得目前匯率。 如果您針對相同的貨幣重複呼叫它,而且它未快取,則您正在執行冗餘工作 (和網路 I/O)。 這很簡單,但效率不高。

  • 使用快取:您可以在擷取之後新增字典來儲存結果,以便後續呼叫可立即從記憶體返回。 此技術會增加一些複雜性(您必須管理快取,如果速率發生變化,可能面臨失效),但對於常請求的資料,它可以透過避免不必要的調用,顯著提高性能。

快取的決定通常取決於使用模式。 程式碼變得稍微複雜一些(您需要處理快取邏輯),並且您必須確保它保持正確(例如,過時的資料、多執行緒存取時的執行緒安全等)。 快取是以多一點點的複雜性換取效能的經典案例。 當重複存取資料時,快取可大幅改善效能。

平衡效率和可讀性的最佳實踐

以下是一些最佳實踐,可協助您平衡程式碼的效率和可讀性:

  • 偏好演算法清晰度: 在選擇如何實現某件事時,首先考慮演算法的複雜性(是線性的、二次的等? 選擇一種既能提供良好複雜性又不會扭曲程式碼的設計。 通常,演算法上最優雅的解決方案也是乾淨的程式碼。

  • 使用適合工作的工具: 高階語言和程式庫提供您應該使用的最佳化功能。 例如,C# 中的語言整合查詢 (LINQ) 可以清楚表達某些資料作業,並且在內部進行了合理的最佳化。 同樣地,平行處理程式庫 (Parallel.ForEach, PLINQ) 可同時執行,同時維持相對簡單的程式碼結構。 除非必要,否則不要重新發明輪子。

  • 評論不明顯的優化:如果您因性能考量而以不直觀的方式進行操作,請添加註釋解釋原因。 範例:「在這裡使用手動物件集區來降低記憶體回收壓力,因為此方法在緊密迴圈中呼叫,而且我們無法承受頻繁的配置。新增註解有助於未來的讀者 (以及您自己在六個月內) 記住程式碼為何那樣寫。

  • 增量改進: 您通常可以從簡單的設計開始,然後逐步改進需要它的部分。 請務必將程式碼的最佳化版本與原始版本進行比較,以確保程式碼行為不變。 如有必要,版本控制可以幫助您復原變更。

  • 不要為了速度而犧牲安全: 例如,跳過輸入驗證或錯誤處理可能會使程式碼運行得更快一些,但幾乎不值得權衡。 穩健性更為重要。 目標是在不破壞程式碼的正確性或安全性的情況下進行最佳化。

避免過早優化

在知道真正的瓶頸在哪裡之前就急於優化程式碼是一個常見的陷阱。

在實務上:

  • 使用良好的結構整潔地撰寫您的程式碼。
  • 識別是否有任何部分是瓶頸。
  • 以可維護的方式優化該部分,並驗證改進。

這種方法可確保您將時間花在重要的事情上,並保持程式碼庫的高效和健康。

總結

編寫高效的程式碼不一定要以犧牲可讀性為代價。 透過優先考慮清晰度並根據證據進行最佳化,您可以實現效能和可維護性的平衡。 使用適當的資料結構,實作內建函式庫,並明智地應用最佳化。 始終記錄不明顯的選擇並避免過早優化。 這種平衡的方法可以產生經得起時間考驗的強大、高效且易於理解的程式碼。