.Net,你爲什麼會慢

自打使用.Net以來,他給我的印象就一直是:慢。不過這幾天看了一下.Net程序運行時的原理,才明白了我們平時的.Net程序是爲什麼慢的,也明白了在某些情況下其實.Net程序運行起來也不比非託管程序慢。

要看託管程序慢的原因,就得說說應用程序加載的過程。

應用程序文件的格式是有規律的。不管是託管程序還是非託管程序,可執行文件的內部都包含一個PE文件(包含在exe文件或者dll文件的內部),系統也正是根據PE文件裏面的信息來啓動這些可執行程序的。系統根據PE文件中的信息,找到入口函數,接着將控制調轉到這個函數中,從而啓動這個程序。不過託管程序的文件中還有一個CLR表頭文件以及其他CLR需要的信息。(有關PE文件的信息,請點擊這裏。個人認爲要真正理解託管程序爲啥慢,下功夫瞭解PE文件及其作用還是很重要的)

首先看看非託管程序。非託管程序的可執行文件都是二進制文件,是直接被編譯成CPU指令的。在非託管程序的可執行文件中,編譯器在編譯的時候已經把對方法的調用直接編譯成了CPU指令:因爲在編譯的時候就知道方法在代碼段裏的相對地址,也就是偏移量。當系統加載了可執行文件後,我們通過將可執行文件的基地址加上這個偏移量就可以計算出方法在內存中的實際地址。這樣只要通過這種方法修改JMP指令,就可以直接運行整個程序。

但託管程序不同。因爲託管程序編譯的結果是IL中間代碼,而這個IL代碼是由CLR實時編譯的,所以在啓動這個程序之前,必須先加載CLR,並由CLR負責處理IL代碼中的方法調用。

那麼,操作系統是如何知道一個應用程序需要加載CLR的呢?也許有人會說因爲託管程序的文件中還有一個CLR頭部,看到這個就知道是託管程序。這個說法當然不對。最新的操作系統也許能夠認出CLR頭部,但2000之前的系統,他們如何會認得出CLR頭部?要知道當這些系統出來的時候,還根本沒有CLR這個玩意兒呢。

實際上,系統啓動一個託管程序,最開始的步驟都是一樣的:檢查PE文件,然後執行PE文件中.text段(也就是代碼段)中的代碼。但託管程序在編譯時,.text段裏面增加了一條JMP _CorExeMain或者JMP _CorDllMain的指令(根據是exe文件還是dll文件不同)。也正是從這裏開始,託管程序的加載與非託管程序的加載產生了區別。這時候如果是非託管程序,就已經進入到入口函數中去了;但託管程序此時卻跳轉到了另一個函數中。那麼這個函數是哪裏的呢?這個函數在一個叫做MSCorEE.dll的動態鏈接庫文件中,當安裝了.net框架時就會被複制在系統目錄下。系統會根據託管程序PE文件中的信息找到這個DLL,然後通過MSCorEE.dll的PE文件信息找到這個_CorExeMain函數的入口地址,然後修改剛纔的JMP指令要跳轉的地址,從而將控制跳轉到了_CorExeMain這個函數裏面去。然後,在這個函數裏面,CLR被啓動了,並做了若干的初始化工作,然後再通過託管程序的CLR表頭找到託管程序的入口地址,並將控制跳轉到這裏,於是託管程序開始運行。

不過,上述過程在最新的操作系統上不同,因爲這些新的操作系統認得託管程序的標誌(也就是說知道根據PE文件中的標誌去判斷是否是託管程序),因此在加載時就會直接調用_CorExeMain,JMP指令直接被跳過的。

剛纔說了託管程序在啓動時的一些特殊處理:系統在進入入口函數前會首先調用MSCorEE.dll中的代碼來啓動CLR並做一些初始工作,然後再進入入口函數。但還有個問題沒提到:那就是剛纔我們有說到託管程序的編譯結果是IL代碼,這個IL代碼是在運行時被CLR實時編譯的。那麼,這個實時編譯的過程又是怎樣的呢?

實際上,IL中的方法並不是每次被調用時都會被重新編譯一次,而是就像我們平時採用的“LazyLoad”一樣,他只有在第一次被調用的時候纔會被編譯。即時編譯器保存有一個映射表。當調用一個方法時,即時編譯器如果發現在這個映射表中沒有標記這個方法,就會將這個方法的IL代碼編譯成CPU指令,然後分配在一個內存空間上,然後在這個映射表中記錄下這個方法名和方法入口對應的內存地址,然後通過JMP指令跳轉到函數中去。當下次再產生對這個方法的調用時,即時編譯器因爲已經知道了這個方法對應的內存地址,因此就會直接通過JMP指令跳轉,而不會再次編譯這段代碼。

因此可以看出,只要程序所有的代碼都被執行過一次了,那麼整個程序就都會被編譯成CPU指令保存在內存中。在此之後託管程序跟非託管程序的執行效率就基本上沒什麼區別了——當然,託管程序需要從那個映射表中取函數地址,而非託管程序中方法的地址是已知的。

因此,理論上在某些情況下(比如win service、iis等長期循環執行的程序),託管程序的性能並不會比非託管程序的性能差多少。而且,非託管程序因爲要考慮兼容性必須兼容標準指令,而非託管程序因爲是運行時編譯的,非常清楚操作系統環境,因此可以做針對性的優化。

不過,因爲即時編譯的結果是保存在內存中的,因此對於那些會頻繁啓動的程序來講,其啓動過程是會比較慢的——因爲每次啓動都需要加載CLR並做一次即時編譯。

至此,在瞭解到了託管程序與非託管程序在加載、執行時的區別,我們就可以更加清楚怎樣才能充分利用非託管程序的優點、避免其缺點,從而發揮他的最大價值、避免使用時走入誤區。

發佈了221 篇原創文章 · 獲贊 3 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章