在應用程式開發過程中,開發人員都力求寫出更加高效的代碼。但是當你想手工為C#編譯器最佳化代碼時,你的種種最佳化可能反倒會阻礙JIT進行更加高效的最佳化。因此,我們最好儘可能的寫出最清晰的代碼,將最佳化工作交給JIT編譯器去完成。
在.NET平台下開發程式的開發人員都應該知道:.NET運行時將調用JIT編譯器來將C#編譯器產生的IL翻譯成機器碼。JIT不會在程式剛開始的時候就完全翻譯所有的IL代碼,CLR根據函數的粒度來逐一進行JIT編譯。沒有被調用的函數根本不會被JIT編譯,因此將那些非常重要的邏輯分解成更多的小方法要比把所有邏輯放在一起形成大型複雜函數更有效率。例如下面的代碼:
1 public string BuildMsg(bool takeFirstPath) 2 { 3 StringBuilder msg = new StringBuilder(); 4 5 if (takeFirstPath) 6 { 7 msg.Append("A problem occurred."); 8 msg.Append("\nThis is a problem."); 9 msg.Append("imagine much more text");10 }11 else12 {13 msg.Append("This Path is not so bad.");14 msg.Append("\nIt is only a minor inconvenience.");15 msg.Append("Add more detailed diagnostics here.");16 }17 return msg;18 }
在第一次調用BuildMsg時,if-else兩個分支都將被JIT編譯。而實際上僅需要編譯其中的一個分支就足夠了,我們可以拆分這個方法,對其進行最佳化,下面是最佳化後的代碼:
View Code
1 public string BuildMsg(bool takeFirstPath) 2 { 3 if (takeFirstPath) 4 { 5 return FirstPath(); 6 } 7 else 8 { 9 return SecondPath();10 }11 }12 13 public string FirstPath()14 {15 StringBuilder msg = new StringBuilder();16 17 msg.Append("A problem occurred.");18 msg.Append("\nThis is a problem.");19 msg.Append("imagine much more text");20 21 return msg.ToString();22 }23 24 public string SecondPath()25 {26 StringBuilder msg = new StringBuilder();27 28 msg.Append("This Path is not so bad.");29 msg.Append("\nIt is only a minor inconvenience.");30 msg.Append("Add more detailed diagnostics here.");31 32 return msg.ToString();33 }34 }
這時候兩個方法可以根據需要再進行JIT編譯,而不必在第一次調用BuildMsg方法是進行。我們可以看出:更小的函數讓JIT編譯器更方便的根據需要進行編譯,而不是將時間浪費在不急於一時使用的代碼上。對於switch語句中的每個case中的代碼,這個規則的影響更明顯。
寄存器的最佳化
小而簡單的方法會讓JIT更容易的進行寄存器的選擇工作,即選擇哪個局部變數可以存放在寄存器中,而不是棧上。越少使用局部變數,也就讓JIT編譯器能夠更方便的找到最適合放在寄存器的那一些。而越小的函數包含的局部變數也越少,也就更方便JIT對寄存器進行最佳化。
內聯的最佳化
內聯表示把函數體替換到函數被調用的位置。由JIT編譯器負責決定哪些方法應該被內聯,當內聯可以有效提高效率時,JIT編譯器將自動執行。不過內聯的標準並不是固定的,且當前的規則也不能保證將來不會發生變化,此外,是否內聯完全由JIT自己決定。不過我們可以使用下面的特性選項通知JIT不要內聯某個方法:
1 [MethodImpl(MethodImplOptions.NoInlining)]
方法越簡單就越適合內聯。不過虛方法和包含ctry/catch代碼塊的方法將不會被內聯。內聯也改變了:代碼在執行時才會被JIT編譯 這一原則。所以在.NET平台下編程我們的責任應該就是盡量編寫短小精悍的方法,而為你的演算法產生高效的機器碼是C#編譯器和JIT編譯器的責任。
小節:
將C#代碼翻譯為可執行檔機器碼有兩個步驟:1.C#編譯器將代碼產生為IL,並放在程式集中。2.JIT再根據需要逐一為方法(或是一組方法,如果涉及內聯)產生機器碼。短小的方法讓JIT編譯器能夠更好的平攤編譯的代價。短小的代碼也更適合內聯。方法除了短小之外,簡化控制流程程也很重要,控制的分支越少JIT編譯器也更容易選擇找到最適合放在寄存器中的變數。因此,編寫短小精悍的代碼不但影響代碼的可讀性,也影響到程式啟動並執行效率。