做為一個有經驗的程序員,不管你在使用C#以前是習慣用什麼語言的,我們 綜合了幾個可以讓你開發出有效代碼的實際方法。有些時候,我們在先前的環境 中所做的努力在.Net環境中卻成了相反的。特別是在你試圖手動去優化一些代碼 時尤其突出。你的這些行為往往會阻止JIT編譯器進行最有效的優化。你的以性 能為由的額外工作,實際上產生了更慢的代碼。你最好還是以你最清楚的方法寫 代碼,其它的讓JIT編譯器來做。最常見的一個例子就是預先優化,你創建一個 很長很復雜的函數,本想用它來避免太多的函數調用,結果會導致很多問題。實 際操作時,提升這樣一個函數的邏輯到循環體中對.Net程序是有害的。這與你的 真實是相反的,讓我們來看一些細節。
這一節介紹一個簡單的內容,那 就是JIT編譯器是如何工作的 。.Net運行時調用JIT編譯器,用來把由C#編譯器 生成的IL指令編譯成機器代碼。這一任務在應用程序的運行期間是分步進行的。 JIT並不是在程序一開始就編譯整個應用程序,取而代之的是,CLR是一個函數接 一個函數的調用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第一次調用時,兩個選擇 項就都編譯了。而實際上只有一個是須要的。但是假設你這樣寫代碼:
public string BuildMsg( bool takeFirstPath )
{
if ( takeFirstPath )
{
return FirstPath( );
} else
{
return SecondPath( );
}
}
因為函數體的每個分支被分解到了獨立的小函數中,而JIT就是 須要這些小函數,這比前面的BuildMsg調用要好。確實,這個例子只是人為的, 而且實際上它也沒什麼太特別的。但想想,你是不是經常寫更“昂貴 ”的例子呢:一個if 語句中是不是每個片段中都包含了20或者更多的語句 呢?你的開銷就是讓JIT在第一次調用它的時候兩個分支都要編譯。如果一個分 支不像是錯誤條件,那到你就招致了本可以簡單避免的浪費。小函數就意味著 JIT編譯器只編譯它要的邏輯,而不是那些沉長的而且又不會立即使用的代碼。 對於很長的switch分支,JIT要花銷成倍的存儲,因此把每個分支的內容定義成 內聯的要比分離成單個函數要好。
JIT編譯器可以更簡單的對小而簡單的 函數進行可登記(enregistration)處理。可登記處理是指進程選擇哪些局部變量 可以被存儲到寄存器中,而這比存儲到堆棧中要好。創建少的局部變量可以能 JIT提供更好的機會把最合適的候選對象放到寄存器中。這個簡單的控制流程同 樣會影響JIT編譯能否如期的進行變量注冊。如果函數只有一個循環,那麼循環 變量就很可能被注冊。然而,當你在一個函數中使用過多的循環時,對於變量注 冊,JIT編譯器就不得不做出一些困難的決擇。簡單就是好,小而簡單的函數很 可能只包含簡單幾個變量,這樣可以讓JIT很容易優化寄存器的使用。
JIT編譯器同樣決定內聯方法。內聯就是說直接使用函數體而不必調用函 數。考慮這個例子:
// readonly name property:
private string _name;
public string Name
{
get
{
return _name;
}
}
// access:
string val = Obj.Name;
相對函數的調用開銷來說,屬性訪問器實體包含更少 數的指令:對於函數調用,要先在寄存器中存儲它的狀態,然後從頭到尾執行, 接著存儲返回結果。這還不談如果有參數時,把參數壓到堆棧上還要更多的工作 。如果你這樣寫,這會產生更多的機器指令:
string val = Obj._name;
當然,你應該不會這樣做,因為你已經明白最好不要 創建公共數據成員(參見原則1)。JIT編譯器明白你即須要效率也須要簡潔,所以 它會內聯屬性訪問器。JIT會在以速度或者大小為目標(或者兩個同時要求)時, 內聯一些方法,用函數體來取代函數的調用會讓它更有利。一般情況不用為內聯 定義額外的規則,而且任何已經實現的內聯在將來都可能被改變。另外,內聯函 數並不是你的職責。正好C#語言沒有提供任何關鍵字讓你暗示編譯器說你想內聯 某個函數。實際上,C#編譯器也不支持任何暗示來讓JIT編譯進行內聯。你可以 做的就是確保你的代碼盡可能的清楚,盡可能讓JIT編譯器容易的做出最好的決 定。我的推薦現在就很熟悉了:越小的方法越有可能成為內聯對象。請記住:任 何虛方法或者含有try/catch塊的函數都不可能成為內聯的。
內聯修改了 代碼正要被JIT的原則。再來考慮這個訪問名字屬性的例子:
string val = "Default Name";
if ( Obj != null )
val = Obj.Name;
JIT編譯器內聯了屬性訪問器, 這必然會在相關的方法被調用時JIT代碼。
你沒有責任來為你的算法決定 最好的機器級別上的表現。C#編譯器以及JIT編譯器一起為你完成了這些。C#編 譯器為每個方法生成IL代碼,而JIT編譯器則把這些IL代碼在目標機器上翻譯成 機器指令。並不用太在意JIT編譯器在各種情況下的確切原則;有這些時間可以 開發出更好的算法。取而代之的,你應該考慮如何以一種好的方式表達你的算法 ,這樣的方式可以讓開發環境的工具以最好的方式工作。幸運的是,這些你所考 慮的這些原則(譯注:JIT工作原則)已經成為優秀的軟件開發實踐。再強調一次 :使用小而簡單的函數。
記住,你的C#代碼經過了兩步才編譯成機器可 執行的指令。C#編譯器生成以程序集形式存在的IL代碼。而JIT編譯器則是在須 要時,以每個函數為單元生成機器指令(當內聯調用時,或者是一組方法)。小函 數可以讓它非常容易被JIT編譯器分期處理。小函數更有可能成為內聯候選對象 。當然並不是足夠小才行:簡單的控制流程也是很重要的。函數內簡單的控制分 支可以讓JIT以容易的寄存變量。這並不是只是寫清晰代碼的事情,也是告訴你 如何創建在運行時更有效的代碼。
返回教程目錄