# 原則11:理解小函數的魅力
**By D.S.Qiu**
尊重他人的勞動,**支持原創,轉載請注明[出處](/blog/1986910):[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
作為一個有經驗的程序員,不管在喜歡 C# 之前用的是什么語言,都會積累開發更高效代碼的經驗。有時,能在之前的環境有效的方法在 .NET 環境中會起反作用。當你想手動優化 C# 編譯器的算法時就會體會到。你的行為經常會阻止 JIT 編譯器更高效的優化。你額外提升性能的工作實際會產生更慢的代碼。你最好的讓你的代碼寫的盡可能地清晰。讓 JIT 做剩下的工作。最常見的一個例子就是創建一個更長更復雜的函數希望能避免函數調用的過早優化會引起問題。像這樣組裝函數邏輯到一個循環體中實際傷害了 .NET 程序的性能。這是很直觀的,所以下面我們對細節進行詳細說明。
.NET 運行時調用 JIT 編譯器翻譯 C# 翻譯器產生的的 IL 代碼為機器碼。這個過程是評價攤銷在程序執行的整個生命周期。在程序啟動時, JITing 不會全部載入你的程序,而是 CLR 是基于一個一個函數調用 JITer 。這最大限度地減少啟動時的開銷至一個合理的水平上。但如果更多代碼需要被 JIT 程序會變得更遲鈍。函數只有被 JIT 后才能被調用。你可以通過拆分代碼成更多更小的函數而不是直接使用更少而大的函數來最大減小被 JIT 產生額外的代碼數量。考慮下面的例子,盡管有點刻意:
```
public string BuildMsg(bool takeFirstPath)
{
StringBuilder msg = new StringBuilder();
if (takeFirstPath)
{
msg.Append("A problem occurred.");
msg.Append("\nThis is a problem.");
msg.Append("imagine much more text");
}
else
{
msg.Append("This path is not so bad.");
msg.Append("\nIt is only a minor inconvenience.");
msg.Append("Add more detailed diagnostics here.");
}
return msg.ToString();
}
```
BuildMsg 第一個調用,兩個分支代碼都會被 JIT 。但只有一個是需要的。但是假設你按下面方式重寫函數:
```
public string BuildMsg2(bool takeFirstPath)
{
if (takeFirstPath)
{
return FirstPath();
}
else
{
return SecondPath();
}
}
```
不過,這個例子有點牽強,而且不會有太大的區別。但是考慮下你經常寫的更廣泛的例子:一個 if 的結構有超過 20 的條件狀態。函數第一個載入是你需要為所有分支花費 JIT 的開銷。如果你分支不大可能是錯誤條件,你可以很容易避免這個開銷。短小的函數意味著 JIT 編譯器只編譯邏輯上需要的函數,而不是你沒有立即使用整個一長串代碼。 switch 結構可以節省好幾被的 JIT 開銷,如果每個 case 條件定義為 inline 而不是 單獨的函數。
短小而簡單的函數可以讓 JIT 編譯器很容易支持寄存器化(enregistration) 。寄存器化是指處理器選擇寄存器而不是棧存儲局部變量。創建更少的局部變量使得 JIT 編譯器更好的找到可用的寄存器。控制流的簡化同一會影響 JIT 編譯使用寄存器存儲變量。如果函數有一個循環體,這個循環變量很可能存儲在寄存器中。然而,當你在一個函數中創建了好幾個循環體, JIT 編譯就要做一個艱難地選擇哪一個循環變量存在寄存器中。越簡單越好。一個簡短的函數很可能包含更少的局部變量,可以使得 JIT 編譯器更容易使用寄存器優化程序。考慮下面的例子:
```
// readonly name property:
public string Name { get;private set;}
// access:
string val = Obj.Name;
```
屬性的訪問器比函數調用包含更少的指令:保持寄存器狀態,執行開始很結尾的代碼,存儲函數的返回值。如果需要有參數的話,還需要在棧上壓人參數這一個步驟。如果使用 public 域的用到指令會變得更少。
當然,你不會那樣做,因為你知道盡少使用 public 數據成員(原則1)。 JIT 編譯器知道你需要兼顧代碼的效率和優雅,所以屬性的訪問器是被內聯調用的。當速度和大小的好處有利于用函數體代替函數調用時, JIT 才會將函數內聯調用。該標準沒有定義任何額外的內聯規則,在將來任何實現都可能改變。而且,內聯函數不是你的責任。 C# 語言甚至沒有提供關鍵用以提示編譯器某個方法應該被聲明為內聯。 C# 編譯器不會通過任何 JIT 有關內聯的提示。(你可以使用 System.Runtime.CompilerServices.MethodImpl 特性,指定方法不被內聯。這是在調試時在函數調用棧保留函數名字的典型做法。
```
[MethodImpl(MethodImplOptions.NoInlining)]
```
所有你需要做的就是保證你的代碼盡可能的清晰,使得 JIT 編譯器可以很容易做最好的決定。現在這個建議應該變得更熟悉:小函數優于更容易被內聯調用。但是記住虛函數或包含 try/catch 塊的函數式不會被內聯的。
內聯修改了執行代碼被 JIT 的原則。再看下 Name 屬性的訪問:
```
string val = "Default Name";
if (Obj != null)
val = Obj.Name;
```
如果 JIT 編譯器將屬性訪問器置為內聯,這必須會 JIT 包含屬性調用的方法的代碼。
建議構建更小,組合的方法在LINQ查詢和函數式編程的世界更為重要。所有的LINQ查詢方法是相當小的。此外,大多數傳遞給LINQ查詢的謂詞,行為,和函數都是很小的代碼塊。小的,更可組合自然意味著這些方法,行為,謂詞和函數更容易重用。此外, JIT 編譯器更有機會優化代碼使得在運行時執行的更有效率。
你不要負責決定最好機器級展示你的算法。 C# 編譯器和 JIT編譯器以前為你做這個。 C# 為每個方法產生 IL 代碼, JIT 在宿主機器上將 IL 代碼方法為機器碼。你不用關心 JIT 編譯器在不同情況下使用的準確規則,這些將隨著時間的推移開發出更好的算法。相反,你應該關心你的表達
算法的方式,使得它容易在環境中的工具做最好的工作。幸運的是,這些規則您已經遵循良好的軟件開發實踐的規則是一致的。更多的時候:更小,更簡單的函數。
JIT 編譯器同時會對是否使用內聯函數做決定。內聯意味著用函數體代替函數的調用。
因為不同分支代碼被分解成各自的函數,這些函數只有在需要的是才被 JIT 而不是第一次 BuildMsg 被調用時。
記住翻譯 C# 代碼到機器可執行碼需要兩個步驟: C# 編譯器產生發布 在程序的 IL 代碼。 JIT 編譯器根據需要為每個方法(或者是內聯調用的函數組)產生機器碼。小函數使得 JIT 編譯器可以更容易分攤開銷。小函數還更有可能使用內聯方式調用。這不只是短小:簡單的控制流會出現更多問題。較少的控制分支可以讓 JIT 編譯器使用寄存器變量。這不僅是一個讓你代碼寫的更加清晰的好方法,這更是如何創建再運行時更加高效的代碼
小結:
終于寫翻譯完第一章共11個原則,哎,晚上脖子兩側開始疼了,希望會沒事的。經常容易心情不好,前幾天一個朋友的鼓勵:就算失去所有,不是還有明天嗎,雖然我現在已經不需要心靈雞湯了,但其中的鼓勵還是可以感知到的,也不早了(又一點了)
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/1986910](/blog/1986910)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable<T> 和 IComparer<T> 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數