【CLR via C#】CSC将源代码编译成托管模块
????? 下圖展示了編譯源代碼文件的過(guò)程。如圖所示,可用支持 CLR 的任何一種語(yǔ)言創(chuàng)建源代碼文件。然后,用一個(gè)對(duì)應(yīng)的編譯器檢查語(yǔ)法和分析源代碼。無(wú)論選用哪一個(gè)編譯器,結(jié)果都是一個(gè)托管模塊(managedmodule)。托管模塊是一個(gè)標(biāo)準(zhǔn)的 32 位 Microsoft Windows 可移植執(zhí)行體(PE32)文件 6 ,或者是一個(gè)標(biāo)準(zhǔn)的 64 位Windows 可移植執(zhí)行體(PE32+)文件,它們都需要 CLR 才能執(zhí)行。順便說(shuō)一句,托管的程序集總是利用了 Windows 的數(shù)據(jù)執(zhí)行保護(hù)(Data Execution Prevention,DEP)和地址空間布局隨機(jī)化(Address SpaceLayout Randomization,ASLR);這兩個(gè)功能旨在增強(qiáng)整個(gè)系統(tǒng)的安全性。
托管模塊的組成部分
PE32 或 PE32+頭:標(biāo)準(zhǔn) Windows PE 文件頭,類似于“公共對(duì)象文件格式(Common Object File Format,COFF)”頭。如果這個(gè)頭使用 PE32 格式,?文件能在Windows的 32 位或 64 位版本上運(yùn)行。如果這個(gè)頭使用 PE32+格式,文件只能在 Windows 的 64 位版本上運(yùn)行。這個(gè)頭還標(biāo)識(shí)了文件類型,包括 GUI,CUI 或者 DLL,并包含一個(gè)時(shí)間標(biāo)記來(lái)指出文件的生成時(shí)間。對(duì)于只包含 IL 代碼的模塊,PE32(+)頭的大多數(shù)信息會(huì)被忽視。對(duì)于包含本地 CPU代碼的模塊,這個(gè)頭包含了與本地 CPU 代碼有關(guān)的信息
CLR 頭:包含使這個(gè)模塊成為一個(gè)托管模塊的信息(可由 CLR 和一些實(shí)用程序進(jìn)行解釋)。頭中包含了需要的 CLR 版本,一些 flag,托管模塊入口方法(Main 方法)的 MethodDef 元數(shù)據(jù) token,以及模塊的元數(shù)據(jù)、資源、強(qiáng)名稱、一些 flag 以及其他不太重要的數(shù)據(jù)項(xiàng)的位置/大小
元數(shù)據(jù):每個(gè)托管模塊都包含元數(shù)據(jù)表。主要有兩種類型的表:一種類型的表描述源代碼中定義的類型和成員,另一種類型的表描述源代碼引用的類型和成員
IL(中間語(yǔ)言)代碼:編譯器編譯源代碼時(shí)生成的代碼。在運(yùn)行時(shí),CLR 將 IL 編譯成本地 CPU指令。
????? 本地代碼編譯器(native code compilers)生成的是面向特定 CPU 架構(gòu)(比如 x86,x64 或 IA64)的代碼。相反,每個(gè)面向 CLR 的編譯器生成的都是 IL(中間語(yǔ)言)代碼。IL 代碼有時(shí)稱為托管代碼,因?yàn)?CLR 要管理它的執(zhí)行。
? 除了生成 IL,面向 CLR 的每個(gè)編譯器還要在每個(gè)托管模塊中生成完整的元數(shù)據(jù)。簡(jiǎn)單地說(shuō),元數(shù)據(jù)(metadata)是一組數(shù)據(jù)表。其中一些數(shù)據(jù)表描述了模塊中定義的內(nèi)容,比如類型及其成員。還有一些元數(shù)據(jù)表描述了托管模塊引用的內(nèi)容,比如導(dǎo)入的類型及其成員。元數(shù)據(jù)是一些老技術(shù)的超集。這些老技術(shù)包括 COM 的“類型庫(kù)(Type Library)”和“接口定義語(yǔ)言(Interface Definition Language,IDL)”文件。要注意的是,CLR 元數(shù)據(jù)遠(yuǎn)比它們完整。另外,和類型庫(kù)及 IDL 不同,元數(shù)據(jù)總是與包含 IL 代碼的文件關(guān)聯(lián)。事實(shí)上,元數(shù)據(jù)總是嵌入和代碼相同的 EXE/DLL 文件中,這使兩者密不可分。由于編譯器同時(shí)生成元數(shù)據(jù)和代碼,把它們綁定一起,并嵌入最終生成的托管模塊,所以元數(shù)據(jù)和它描述的 IL 代碼永遠(yuǎn)不會(huì)失去同步。元數(shù)據(jù)有多種用途,下面僅列舉一部分。
*? 編譯時(shí),元數(shù)據(jù)消除了對(duì)本地 C/C++頭和庫(kù)文件的需求,因?yàn)樵谪?fù)責(zé)實(shí)現(xiàn)類型/成員的 IL 代碼文件中,已包含和引用的類型/成員有關(guān)的全部信息。編譯器可直接從托管模塊讀取元數(shù)據(jù)。
*? Microsoft Visual Studio 使用元數(shù)據(jù)幫助你寫代碼。它的“智能感知(IntelliSense)”技術(shù)可以解析元數(shù)據(jù),指出一個(gè)類型提供了哪些方法、屬性、事件和字段。如果是一個(gè)方法,還能指出方法需要什么參數(shù)。
*? CLR 的代碼驗(yàn)證過(guò)程使用元數(shù)據(jù)確保代碼只執(zhí)行“類型安全”的操作。(稍后就會(huì)講到驗(yàn)證。)。
* 元數(shù)據(jù)允許將一個(gè)對(duì)象的字段序列化到一個(gè)內(nèi)存塊中,將其發(fā)送給另一臺(tái)機(jī)器,然后反序列化,在遠(yuǎn)程機(jī)器上重建對(duì)象的狀態(tài)。
*? 元數(shù)據(jù)允許垃圾回收器跟蹤對(duì)象的生存期。垃圾回收器能判斷任何對(duì)象的類型,并從元數(shù)據(jù)知道那個(gè)對(duì)象中的哪些字段引用了其他對(duì)象
將托管模塊合并成程序集
CLR 實(shí)際不和模塊一起工作。相反,它是和程序集一起工作的。程序集(assembly)是一個(gè)抽象的概念,初學(xué)者往往很難把握它的精髓。首先,程序集是一個(gè)或多個(gè)模塊/資源文件的邏輯性分組。其次,程序集是重用、安全性以及版本控制的最小單元。取決于你對(duì)于編譯器或工具的選擇,既可以生成單文件程序集,
也可以生成多文件程序集。在 CLR 的世界中,程序集相當(dāng)于一個(gè)“組件”。
下圖有助于理解程序集。在這幅圖中,一些托管模塊和資源(或數(shù)據(jù))文件準(zhǔn)備交由一個(gè)工具處理。該工具生成單獨(dú)一個(gè) PE32(+)文件來(lái)表示文件的邏輯性分組。實(shí)際發(fā)生的事情是,這個(gè) PE32(+)文件包含一個(gè)名為“清單”(manifest)的數(shù)據(jù)塊。清單是由元數(shù)據(jù)表構(gòu)成的另一種集合。這些表描述了構(gòu)成程序集的文件,由程序集中的文件實(shí)現(xiàn)的公開導(dǎo)出的類型 7 ,以及與程序集關(guān)聯(lián)在一起的資源或數(shù)據(jù)文件。
默認(rèn)是由編譯器將生成的托管模塊轉(zhuǎn)換成程序集。換言之,C#編譯器生成含有清單的一個(gè)托管模塊。清單指出程序集只由一個(gè)文件構(gòu)成。
加載公共語(yǔ)言運(yùn)行時(shí)
你生成的每個(gè)程序集既可以是一個(gè)可執(zhí)行應(yīng)用程序,也可以是一個(gè) DLL(其中含有一組由可執(zhí)行程序使用的類型)。當(dāng)然,最終是由 CLR 管理這些程序集中的代碼的執(zhí)行。這意味著必須在目標(biāo)機(jī)器上安裝好.NETFramework。
C#編譯器生成的程序集要么包含一個(gè) PE32 頭,要么包含一個(gè) PE32+頭。除此之外,編譯器還會(huì)在頭中指定要求什么 CPU 架構(gòu)(如果使用默認(rèn)值 anycpu,則不明確指定)。Microsoft發(fā)布了 SDK 命令行實(shí)用程序 DumpBin.exe 和 CorFlags.exe,可用它們檢查編譯器生成的托管模塊所嵌入的信息。
執(zhí)行程序集的代碼
??? 為了執(zhí)行一個(gè)方法,首先必須把它的 IL 轉(zhuǎn)換成本地 CPU 指令。這是 CLR 的 JIT (just-in-time 或者“即時(shí)”)編譯器的職責(zé)。下圖展示了一個(gè)方法首次調(diào)用時(shí)發(fā)生的事情。
???? 就在 Main 方法執(zhí)行之前,CLR 會(huì)檢測(cè)出 Main 的代碼引用的所有類型。這導(dǎo)致 CLR 分配一個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu),它用于管理對(duì)所引用的類型的訪問(wèn)。在圖中,Main 方法引用了一個(gè) Console 類型,這導(dǎo)致 CLR分配一個(gè)內(nèi)部結(jié)構(gòu)。在這個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu)中,Console 類型定義的每個(gè)方法都有一個(gè)對(duì)應(yīng)的記錄項(xiàng) 10 。每個(gè)記錄項(xiàng)都容納了一個(gè)地址,根據(jù)此地址即可找到方法的實(shí)現(xiàn)。對(duì)這個(gè)結(jié)構(gòu)進(jìn)行初始化時(shí),CLR 將每個(gè)記錄項(xiàng)都設(shè)置成(指向)包含在 CLR 內(nèi)部的一個(gè)未文檔化的函數(shù)。我將這個(gè)函數(shù)稱為 JITCompiler。
JITCompiler 函數(shù)被調(diào)用時(shí),它知道要調(diào)用的是哪個(gè)方法,以及具體是什么類型定義了該方法。然后,JITCompiler 會(huì)在定義(該類型的)程序集的元數(shù)據(jù)中查找被調(diào)用的方法的 IL。接著,JITCompiler 驗(yàn)證 IL 代碼,并將 IL 代碼編譯成本地 CPU 指令。本地 CPU 指令被保存到一個(gè)動(dòng)態(tài)分配的內(nèi)存塊中。然后,JITCompiler返回 CLR 為類型創(chuàng)建的內(nèi)部數(shù)據(jù)結(jié)構(gòu),找到與被調(diào)用的方法對(duì)應(yīng)的那一條記錄,修改最初對(duì) JITCompiler 的引用,讓它現(xiàn)在指向內(nèi)存塊(其中包含了剛才編譯好的本地 CPU 指令)的地址。最后,JITCompiler 函數(shù)跳轉(zhuǎn)到內(nèi)存塊中的代碼。這些代碼正是 WriteLine 方法(獲取單個(gè) String 參數(shù)的那個(gè)版本)的具體實(shí)現(xiàn)。這些代碼執(zhí)行完畢并返回時(shí),會(huì)返回至 Main 中的代碼,并跟往常一樣繼續(xù)執(zhí)行?,F(xiàn)在,Main 要第二次調(diào)用 WriteLine。這一次,由于已對(duì) WriteLine 的代碼進(jìn)行了驗(yàn)證和編譯,所以會(huì)
直接執(zhí)行內(nèi)存塊中的代碼,完全跳過(guò) JITCompiler 函數(shù)。WriteLine 方法執(zhí)行完畢之后,會(huì)再次返回 Main。
??? 下圖展示了第二次調(diào)用 WriteLine 時(shí)發(fā)生的事情。
?
一個(gè)方法只有在首次調(diào)用時(shí)才會(huì)造成一些性能損失。以后對(duì)該方法的所有調(diào)用都以本地代碼的形式全速運(yùn)行,無(wú)需重新驗(yàn)證 IL 并把它編譯成本地代碼。JIT 編譯器將本地 CPU 指令存儲(chǔ)到動(dòng)態(tài)內(nèi)存中。一旦應(yīng)用程序終止,編譯好的代碼也會(huì)被丟棄。所以,如果將來(lái)再次運(yùn)行應(yīng)用程序,或者同時(shí)啟動(dòng)應(yīng)用程序的兩個(gè)實(shí)例(使用兩個(gè)不同的操作系統(tǒng)進(jìn)程),JIT 編譯器必須再次將 IL 編譯成本地指令。對(duì)于大多數(shù)應(yīng)用程序,因 JIT 編譯造成的性能損失并不顯著。大多數(shù)應(yīng)用程序都會(huì)反復(fù)調(diào)用相同的方法。
在應(yīng)用程序運(yùn)行期間,這些方法只會(huì)對(duì)性能造成一次性的影響。另外,在方法內(nèi)部花費(fèi)的時(shí)間很有可能比花在調(diào)用方法上的時(shí)間多得多。還要注意的是,CLR 的 JIT 編譯器會(huì)對(duì)本地代碼進(jìn)行優(yōu)化,這類似于非托管 C++編譯器的后端所做的工作。同樣地,可能要花費(fèi)較多的時(shí)間來(lái)生成優(yōu)化的代碼。但是,和沒有優(yōu)化時(shí)相比,代碼在優(yōu)化之后將獲得更出色的性能。
?????? 有兩個(gè) C#編譯器開關(guān)會(huì)影響代碼的優(yōu)化:/optimize 和/debug。下面總結(jié)了這些開關(guān)對(duì) C#編譯器生成
的 IL 代碼的質(zhì)量的影響,以及對(duì) JIT 編譯器生成的本地代碼的質(zhì)量的影響。
???? 雖然這樣說(shuō)很難讓人信服,但許多人(包括我)都認(rèn)為托管應(yīng)用程序的性能實(shí)際上超過(guò)了非托管應(yīng)用程序。有許多原因使我們對(duì)此深信不疑。例如,當(dāng) JIT 編譯器在運(yùn)行時(shí)將 IL 代碼編譯成本地代碼時(shí),編譯器對(duì)執(zhí)行環(huán)境的認(rèn)識(shí)比非托管編譯器更加深刻。下面列舉了托管代碼相較于非托管代碼的優(yōu)勢(shì):
?? 1、JIT 編譯器能判斷應(yīng)用程序是否運(yùn)行在一個(gè) Intel Pentium 4 CPU 上,并生成相應(yīng)的本地代碼來(lái)利用Pentium 4 支持的任何特殊指令。相反,非托管應(yīng)用程序通常是針對(duì)具有最小功能集合的 CPU 編譯的,不會(huì)使用能提升應(yīng)用程序性能的特殊指令。
?? 2、JIT 編譯器能判斷一個(gè)特定的測(cè)試在它運(yùn)行的機(jī)器上是否總是失敗。例如,假定一個(gè)方法包含以下代碼:
if (numberOfCPUs > 1) {
...
}
???? 如果主機(jī)只有一個(gè) CPU,JIT 編譯器不會(huì)為上述代碼生成任何 CPU 指令。在這種情況下,本地代碼將針對(duì)主機(jī)進(jìn)行優(yōu)化,最終的代碼變得更小,執(zhí)行得更快。
?? 3、應(yīng)用程序運(yùn)行時(shí),CLR 可以評(píng)估代碼的執(zhí)行,并將 IL 重新編譯成本地代碼。重新編譯的代碼可以重新組織,根據(jù)剛才觀察到的執(zhí)行模式,減少不正確的分支預(yù)測(cè)。雖然目前版本的 CLR 還不能做到這一點(diǎn),但將來(lái)的版本也許就可以了。
?????除了這些理由,還有另一些理由使我們相信在執(zhí)行效率上,未來(lái)的托管代碼會(huì)比當(dāng)前的非托管代碼更優(yōu)秀。大多數(shù)托管應(yīng)用程序目前的性能已相當(dāng)不錯(cuò),將來(lái)還有望進(jìn)一步提升。
IL? 和驗(yàn)證
IL 是基于棧的。這意味著它的所有指令都要將操作數(shù)壓入(push)一個(gè)執(zhí)行棧,并從棧彈出(pop)結(jié)果。由于 IL 沒有提供操作寄存器的指令,所以人們可以很容易地創(chuàng)建新的語(yǔ)言和編譯器,生成面向 CLR 的代碼。
????? IL 指令還是“無(wú)類型”(typeless)的。例如,IL 提供了一個(gè) add 指令,它的作用是將壓入棧的最后兩個(gè)操作數(shù)加到一起。add 指令不分 32 位和 64 位版本。add 指令執(zhí)行時(shí),它判斷棧中的操作數(shù)的類型,并執(zhí)行恰當(dāng)?shù)牟僮鳌?br />
????? 我個(gè)人認(rèn)為,IL 最大的優(yōu)勢(shì)并不在于它對(duì)底層 CPU 的抽象。IL 提供的最大的優(yōu)勢(shì)在于應(yīng)用程序的健壯性 11 和安全性。將 IL 編譯成本地 CPU 指令時(shí),CLR 會(huì)執(zhí)行一個(gè)名為驗(yàn)證(verification)的過(guò)程。這個(gè)過(guò)程會(huì)檢查高級(jí) IL 代碼,確定代碼所做的一切都是安全的。例如,驗(yàn)證會(huì)核實(shí)調(diào)用的每個(gè)方法都有正確數(shù)量的參數(shù),傳給每個(gè)方法的每個(gè)參數(shù)都具有正確的類型,每個(gè)方法的返回值都得到了正確的使用,每個(gè)方法都有一個(gè)返回語(yǔ)句,等等。在托管模塊的元數(shù)據(jù)中,包含了要由驗(yàn)證過(guò)程使用的所有方法和類型信息。
本地代碼生成器:NGen.exe
???? 使用.NET Framework 配套提供的 NGen.exe 工具,可以在一個(gè)應(yīng)用程序安裝到用戶的計(jì)算機(jī)上時(shí),將 IL代碼編譯成本地代碼。由于代碼在安裝時(shí)已經(jīng)編譯好,所以 CLR 的 JIT 編譯器不需要在運(yùn)行時(shí)編譯 IL 代碼,這有助于提升應(yīng)用程序的性能。NGen.exe 能在兩種情況下發(fā)揮重要作用:
?? 1、加快 應(yīng)用程序的啟動(dòng)速度 運(yùn)行 NGen.exe 能加快啟動(dòng)速度,因?yàn)榇a已編譯成本地代碼,運(yùn)行時(shí)不需要再花時(shí)間編譯。
?? 2、減小應(yīng)用程序的工作集 13 如果一個(gè)程序集會(huì)同時(shí)加載到多個(gè)進(jìn)程中,對(duì)該程序集運(yùn)行 NGen.exe可減小應(yīng)用程序的工作集(working set)。NGen.exe 會(huì)將 IL 編譯成本地代碼,并將這些代碼保存到一個(gè)單獨(dú)的文件中。這個(gè)文件可以通過(guò)“內(nèi)存映射”的方式,同時(shí)映射到多個(gè)進(jìn)程地址空間中,使代碼得到了共享,避免每個(gè)進(jìn)程都需要一份單獨(dú)的代碼拷貝。
總結(jié)
以上是生活随笔為你收集整理的【CLR via C#】CSC将源代码编译成托管模块的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Unity FixedUpdate 与
- 下一篇: mysql timeout知多少