就大多數的情形而言,應該這麼說:如果不曾試著了解一些底層的運作機制或 API 的注意事項,加上寫程式時漫不經心,的確很容易寫出效能不佳的 .NET 程式。
也就是說,程式跑得太慢,原因大多與程式碼的寫法以及架構設計之優劣有關。如果不先找到問題癥結,而單純以為把 managed code 預先編譯成 native code 來執行就能大幅提升效能,最終很可能白忙一場。
進一步說,當你使用 C#、VB、或其他 .NET 程式語言來開發 .NET 應用程式時,編譯器會將這些高階語言的程式碼轉換成中介語言(Intermediate Language;簡稱 IL)代碼。等到真正執行 .NET 應用程式時,這些 IL 代碼則會被即時編譯成機器碼(例如 x86、x64 組合語言)。其中負責即時編譯工作的元件就是「JIT 編譯器」(Just-In-Time compiler),或暱稱 jitter。
這個 jitter 有個聰明的地方:它是採取類似漸進式編譯(需要時才編譯)的策略,而不是一股腦兒把當前欲執行的 .NET 應用程式全部編譯成機器碼。而且,同一個區塊的 IL 代碼只會編譯一次。舉例來說,執行 Console 應用程式時,進入點 Main() 函式所用到的相關型別會先被 jitter 編譯,之後隨著程式執行的過程,碰到尚未編譯過的型別時,才會編譯那些型別的 IL 代碼。
這表示 .NET 應用程式在第一次載入時會比較花時間,而一旦某型別的 IL 代碼已經編譯過,其執行速度應該非常趨近 native code。換言之,如果你發現應用程式的某項功能無論執行幾次都是那麼慢,此時應查看程式寫法或架構設計有沒有問題,而不是在 JIT 編譯機制上打轉。
NGen.exe 能解決你的效能問題嗎?
也許可以,但我想多半還是不適用。
何以見得?
首先,NGen 工具的使用場合,主要是在用戶端環境下產生預先編譯過的 DLL,這些預先編譯過的 DLL 如同 .NET 組件的快取版本。當 NGen 進行編譯時,係針對用戶端當時的作業環境來產生 native code。如果後來用戶端作業環境有了某些變動(例如 CPU 換了、Windows 作業系統版本升級了),就必須重新再執行一遍 NGen 編譯程序,否則 CLR 會放棄原先 NGen 預先編譯好的檔案,而改採預設的即時編譯模式。這也就失去 NGen 預先編譯的作用了。
那麼,用戶端作業環境的哪些變動會造成 CLR 回頭使用即時編譯呢?包括以下幾種情形:
換言之,如果你想要在開發環境的某台建置伺服器上面用 NGen 預先編譯 EXE/DLL,然後把這些編譯的結果直接部署到多個用戶端,恐怕是白費功夫。
其次,NGen 為了降低對特定硬體環境的假設,在編譯時反而不像 CLR 的 jitter 那樣能夠針對當時的硬體環境產生最佳化的機器碼。舉例來說,由於 NGen 無法得知靜態欄位(static fields)在執行時期的實際記憶體位址,故只能加入一些間接存取的指令,而無法產生最佳效能的機器碼。
因此,對於原本設計不良的的應用程式,即使用 NGen 工具來預先編譯 IL 代碼也很難獲得明顯改善,頂多稍微縮短應用程式的載入時間而已。
參考資料
進一步說,當你使用 C#、VB、或其他 .NET 程式語言來開發 .NET 應用程式時,編譯器會將這些高階語言的程式碼轉換成中介語言(Intermediate Language;簡稱 IL)代碼。等到真正執行 .NET 應用程式時,這些 IL 代碼則會被即時編譯成機器碼(例如 x86、x64 組合語言)。其中負責即時編譯工作的元件就是「JIT 編譯器」(Just-In-Time compiler),或暱稱 jitter。
這個 jitter 有個聰明的地方:它是採取類似漸進式編譯(需要時才編譯)的策略,而不是一股腦兒把當前欲執行的 .NET 應用程式全部編譯成機器碼。而且,同一個區塊的 IL 代碼只會編譯一次。舉例來說,執行 Console 應用程式時,進入點 Main() 函式所用到的相關型別會先被 jitter 編譯,之後隨著程式執行的過程,碰到尚未編譯過的型別時,才會編譯那些型別的 IL 代碼。
這表示 .NET 應用程式在第一次載入時會比較花時間,而一旦某型別的 IL 代碼已經編譯過,其執行速度應該非常趨近 native code。換言之,如果你發現應用程式的某項功能無論執行幾次都是那麼慢,此時應查看程式寫法或架構設計有沒有問題,而不是在 JIT 編譯機制上打轉。
NGen.exe 能解決你的效能問題嗎?
也許可以,但我想多半還是不適用。
何以見得?
首先,NGen 工具的使用場合,主要是在用戶端環境下產生預先編譯過的 DLL,這些預先編譯過的 DLL 如同 .NET 組件的快取版本。當 NGen 進行編譯時,係針對用戶端當時的作業環境來產生 native code。如果後來用戶端作業環境有了某些變動(例如 CPU 換了、Windows 作業系統版本升級了),就必須重新再執行一遍 NGen 編譯程序,否則 CLR 會放棄原先 NGen 預先編譯好的檔案,而改採預設的即時編譯模式。這也就失去 NGen 預先編譯的作用了。
那麼,用戶端作業環境的哪些變動會造成 CLR 回頭使用即時編譯呢?包括以下幾種情形:
- CPU 類型。
- Windows 作業系統版本。
- .NET 組件的模組識別版本編號 (MVID)此編號會在每次重新編譯組件時改變。
- 參考的外部 .NET 組件 的 MVID。
換言之,如果你想要在開發環境的某台建置伺服器上面用 NGen 預先編譯 EXE/DLL,然後把這些編譯的結果直接部署到多個用戶端,恐怕是白費功夫。
因此,對於原本設計不良的的應用程式,即使用 NGen 工具來預先編譯 IL 代碼也很難獲得明顯改善,頂多稍微縮短應用程式的載入時間而已。
參考資料
- CLR via C# 4th Edition by Jeffrey Richter
- Writing High-Performance .NET Code by Ben Watson
沒有留言: