編寫高性能.net代碼-JIT
? ? ?哈哈,本章就當了解吧,底層的內容需要自己去單獨學習了


托管程序在運行時會加載CLR,CLR會先執(zhí)行一些封裝代碼,都是匯編代碼。程序集的托管代碼在第一次運行時,都會執(zhí)行一小段調用即時編譯器(Just-in-Time,JIT)的“樁”代碼(Stub),JIT會把方法的IL轉換為硬件匯編指令
●大部分時候,每個方法只需要經(jīng)過一次JIT編譯。但如果方法帶有泛型(Generic Type)參數(shù)則不一定,這時有可能會對每一種不同類型的參數(shù)都調用一次JIT
●如果你的應用程序或者用戶很在意第一次JIT編譯造成的延時,那么你就需要特別關注一下。大部分應用程序只是關心穩(wěn)定運行期間的性能,但如果你需要很高的可用性,那么JIT可能會成為一個需要優(yōu)化的問題

●3.1 JIT編譯的好處
●引用的就近訪問可能性很高——一起調用的代碼常常會存放在同一個內存頁中,避免了缺頁中斷的開銷
●內存占用降低——只會對真正用到的方法進行編譯
●交叉匯編內聯(lián)化(Cross-assembly Inlining)——可以把其他DLL中的方法(包括 .NET Framework中的)內嵌到你的應用程序中,以顯著提升性能
●可以針對硬件特性進行優(yōu)化,但在實踐中針對特定平臺的優(yōu)化十分有限
●.NET中的大部分代碼優(yōu)化都不是在語言的編譯器中實現(xiàn)的(從C#/VB.NET到IL),而是發(fā)生在JIT編譯器的即時編譯過程中
●3.2 JIT編譯的開銷
●JIT在幕后的處理過程

●3.3 JIT編譯器優(yōu)化
●JIT編譯器會進行一些標準的代碼優(yōu)化工作,比如方法內聯(lián)和去除數(shù)組范圍檢查代碼
●JIT最大的優(yōu)化工作就是方法內聯(lián)
就是把方法體內的代碼嵌入調用的位置,避免原先的方法調用。對于那些會被頻繁調用的小型函數(shù)而言,代碼內聯(lián)比較有意義,因為小型函數(shù)的調用開銷會大于代碼本身的執(zhí)行開銷
●以下這些情況都會阻止方法內聯(lián)的發(fā)生
●虛方法
●同一處調用了接口(Interface)的多種實現(xiàn)代碼
●循環(huán)
●異常處理函數(shù)
●遞歸
●方法體的IL編碼大于32個字節(jié)

●3.4 減少JIT編譯時間和程序啟動時間
JIT編譯器關乎性能的另一個主要因素是生成代碼所耗費的時間,歸根結底最主要還是取決于需要編譯的代碼量
●特別注意
可能有大量代碼是你看不到的,實際執(zhí)行的代碼明顯要比源代碼中看到的要多得多。對全部隱藏代碼進行JIT編譯可能需要耗費相當多的時間。特別是正則表達式和代碼自動生成,有可能會生成大量重復的代碼
●LINQ
LINQ簡潔的語法,把執(zhí)行查詢時實際運行的代碼數(shù)量隱藏了起來。LINQ還把生成委托、分配內存等行為也都隱藏了。簡單的LINQ查詢也許沒有問題,但最重要的一點是你應該進行確切的性能評估
●dynamic關鍵字
dynamic關鍵字帶來的主要問題,也是因為它會轉換為大量的代碼
●正則表達式
正則表達式會轉換為一個動態(tài)程序集中的IL狀態(tài)機,然后進行JIT編譯。這在一開始會多花一些時間,但重復執(zhí)行時就能節(jié)省很多時間
●代碼自動生成
●還有JIT之外的因素也會影響啟動時間
●比如I/O就會增加啟動開銷
●PerfView 分析JIT代碼樁
?

●3.5 利用Profile優(yōu)化JIT編譯
Profile文件可被用于代碼執(zhí)行之前的生成過程。Profile的記錄過程運行在獨立的線程中,保存下來的Profile可以讓生成的代碼獲得與JIT編譯相同的就近訪問可能性(Locality)。該Profile在程序每次執(zhí)行時都會自動更新
●啟用Profile
ProfileOptimization.SetProfileRoot(@”C:\MyAppProfile”); ProfileOptimization.StartProfile(“default”);
●3.6 使用NGEN的時機(簡略)
NGEN把IL匯編代碼轉換為本機映像,實際上就是運行JIT編譯器并把編譯結果保存到本機映像程序集緩存目錄中
●NGEN一般是作為最后的手段來使用
●第一個缺點就是喪失了對象引用的就近訪問可能性
●你可能還失去了某些優(yōu)化效果,比如交叉匯編的內聯(lián)化
●如果應用程序的啟動(“預熱”)開銷太高,上一節(jié)所述的Profile優(yōu)化效果也無法滿足性能需求,可能就該輪到NGEN上場了
●決定使用NGEN之前,請牢記性能優(yōu)化的基本指導原則是:評估、評估、再評估
●3.6.2 本機代碼生成

●3.7 JIT無法勝任的場合
●比如有一些處理器指令JIT是不會去使用的,即便當前的處理器能夠支持也不會。數(shù)量最多的就是大部分SSE和SIMD指令
●JIT編譯器另一個不如本機代碼編譯器的地方,就是托管數(shù)組與直接訪問本機內存的對比
直接訪問本機內存通常意味著無需復制內存,而托管代碼卻是需要封送(Marshalling)的。雖然有辦法繞過內存復制,比如用UnmanagedMemoryStream把本機緩沖區(qū)封裝為Stream,但這樣實際上你是在做不安全的內存訪問
●果應用程序要執(zhí)行大量的數(shù)組或矩陣計算,你就不得不在性能和安全性之間做出平衡
●如果你真的發(fā)現(xiàn)本機代碼的效率要更高一些,可以嘗試用P/Invoke把所有數(shù)據(jù)封送給本機代碼編寫的函數(shù),把計算交給高度優(yōu)化的C++ DLL完成,然后把結果返回給托管代碼

●3.8 評估
●3.8.1 性能計數(shù)器
●# of IL Bytes Jitted
●# of Methods Jitted
●%Time in Jit
●IL Bytes Jitted / sec
●Standard Jit Failures
●Total # of IL Bytes Jitted(和“# of IL Bytes Jitted”完全一樣)
●%Time Loading
●Bytes in Loader Heap
●Total Assemblies
●Total Classes Loaded
●3.8.2 ETW事件
利用ETW事件,對每一個JIT編譯過的方法,你都可以得到大量詳細的性能數(shù)據(jù),包括IL代碼大小和JIT編譯耗時
●MethodID——該方法的唯一ID。
●ModuleID——該方法所屬模塊(Module)的唯一ID。
●MethodILSize——該方法IL代碼的大小。
●MethodNameSpace——與該方法關聯(lián)的完整命名空間名稱。
●MethodName——方法名稱。
●MethodSignature——方法的簽名,以逗號分隔的類型名稱列表
●MethodFlags:
●◎0x1——動態(tài)方法。
●◎0x2——泛型方法。
●◎0x4——經(jīng)JIT編譯的方法(否則為NGEN處理過的方法)。
●◎0x8——JIT助手方法。
●3.8.3 找出JIT耗時最長的方法和模塊
JIT的耗時通常與方法中的IL指令數(shù)量直接相關,但由于類型加載時間也可能包含在JIT耗時中,問題就變得復雜了,特別是當模塊被第一次用到時
●3.9 小結
●請認真考慮那些有可能大量自動生成的代碼
●Profile優(yōu)化可以對絕大部分使用到的代碼進行并行JIT編譯,可用于減少應用程序的啟動時間
●如果要從函數(shù)內聯(lián)中獲得性能收益,請避免用到虛方法、循環(huán)、異常處理、遞歸和代碼較多的方法體
●對于大型應用,或者對程序啟動階段的JIT開銷無法容忍,可以考慮使用NGEN,在使用NGEN之前,請用MPGO對本機映像進行優(yōu)化

本文使用 文章同步助手 同步