ILBC 運行時 (ILBC Runtime) 架構

本文是 VMBC / D# 項目 的 系列文章,html

有關 VMBC / D# , 見 《我發起並創立了一個 VMBC 的 子項目 D#》(如下簡稱 《D#》)  http://www.javashuo.com/article/p-zziqptgy-s.html   。小程序

 

ILBC 運行時       架構圖    以下:數組

                     

 

爲了便於講解,   圖中 一些位置 標註了 紅色數字 。架構

 

ILBC 運行時  包含  3 個 部分:   調度程序 、 InnerC(Byte Code to Native Code) 、 GC  。函數

 

1 處,  調度程序 調用 入口程序集 的  ILBC_Main()  函數, 開始執行程序 。性能

若是 入口程序集 是 ILBC 程序集, 就會 調用  InnerC(Byte Code to Native Code) 編譯  ILBC 程序集 爲 本地程序集(2 處) 。優化

ILBC 程序集 就是  ILBC Byte Code 程序集,  本地程序集 就是 本地代碼 程序集  。spa

若是 入口程序集 是 ILBC 程序集, 就直接調用  ILBC_Main()  函數, 開始執行程序 。操作系統

 

3 處 表示  A 程序集 引用了  B 程序集,  在 調度程序 加載 A 程序集 的 時候, 會調用 A 本地程序集 的  ILBC_GetAssembly()  函數,線程

ILBC_GetAssembly()  函數   以前沒有提到, 如今補充上來 。

ILBC_GetAssembly()  函數   會返回 A 程序集 引用 的 程序集 列表,  包含了 這些 程序集 的 名字 。

程序集 列表 是一個 數組, 數組元素 是 一個 字符數組 的 首地址,  這個 字符數組 就是  程序集 的 名字 。

調度程序 會 根據 程序集列表 去 加載 列表 裏的 程序集,

假設 A 程序集 引用了 B 程序集, 則 程序集 列表 裏有 B,   調度程序 會先把 B 加載到內存, 若是 B 是 本地代碼程序集, 則 直接加載到內存, 若是 B 是 ILBC 程序集, 則 先 JIT 編譯 爲 本地代碼程序集, 再加載到內存 。

4 處 表示 ILBC 程序集 JIT 編譯 爲 本地程序集 後 投入使用 。

 

把 B 加載到 內存後, 調用 B 的  ILBC_GetMethodList()  函數,  返回 B 的 函數表 首地址, 另外一方面, 調度程序 會 調用 A 的  ILBC_GetMethodListList()  函數, 返回  「函數表 列表」  的 首地址,   「函數表 列表」  是  一個數組, 數組元素 是 函數表 首地址,  因此是  「函數表 的 列表」  。

這樣, 把 B 的 函數表 首地址 存到     函數表 列表 中  B  的 位置,   加載 A 和 「依賴項」 B 的 過程 就完成了 。

若是 A 還引用了 其它 程序集,  或者 B 引用了 其它 程序集,  也是 按照 這個 過程 依次加載  。

 

上面這個 過程 說的有點囉嗦, 沒事, 咱們先來看一下   InnerC  的架構,  等下再把這個流程 總結一遍  。

InnerC  的 架構以下:

                       

 

InnerC 分爲  2 個 模塊 :

1   InnerC  to  Byte Code

2   Byte Code  to  Native Code

 

InnerC  to  Byte Code   的 職責 是 語法分析 和 類型檢查,  語法分析 包含了 語法檢查 。

經過 語法分析, 把  C 代碼 解析 爲 表達式對象樹,  而後 對 表達式對象樹 進行 類型檢查,

類型檢查 經過後,  就能夠 返回 表達式對象樹 了,

 

表達式對象樹 能夠直接 傳給   Byte Code  to  Native Code, 

Byte Code  to  Native Code   負責 將 表達式 生成爲 目標代碼 和 連接(連接外部庫), 最終 生成 本地庫,

這就是 AOT 編譯  。

 

表達式對象樹 也能夠 序列化, 序列化 獲得的 byte 數組(byte [ ]) 就是 Byte Code, Byte Code 保存爲 文件 就是  ILBC 程序集 。

ILBC 程序集 能夠 讀取爲 byte 數組(byte [ ]),  byte 數組 反序列化 就是   表達式對象樹,  表達式對象樹  傳給  Byte Code  to  Native Code  編譯爲 本地庫,

這就是 JIT 編譯  。

 

C 代碼 是 第一級 中間代碼,   Byte Code 是 第二級 中間代碼  。

 

這就是  InnerC  的 架構,  以及  AOT 編譯  和  JIT 編譯 的 原理  。

 

咱們能夠把   C 中間代碼 文件 的 擴展名 定義爲  .ilc , 意思是 「ILBC C Code」,

把   ILBC 程序集 (Byte Code 文件) 的 擴展名 定義爲  .ilb,  意思是 「ILBC Byte Code」 。

本地代碼 程序集 的 擴展名 遵循 操做系統 的 規定, 好比 Windows 上 就是 動態連接庫  .dll,  由於 本地程序集 就是 操做系統 定義的 動態連接庫 。

 

咱們 接下來 把     ILBC 運行時  加載 程序集 和 運行 應用程序 的 流程 總結一下 :

1   調度程序 加載 入口程序集, 若是 入口程序集 是 本地程序集, 就 直接加載到內存,

     若是 入口程序集 是 ILBC 程序集, 則 先 JIT 編譯, 把 入口程序集 編譯爲 本地程序集 再加載到內存 。

2   調度程序 調用 入口程序集 的   ILBC_GetAssemblyList() 函數 ,  ILBC_GetAssemblyList() 函數  返回   AssemblyList   首地址  。

      AssemblyList  是一個 數組,  數組元素 是一個  char 數組(char [ ]) 的 首地址,  表示   Assembly 的 名字 (文件名, 不包含擴展名)  。

3   調度程序 用  Assembly 名字 查找 當前目錄下 的 程序集, 先查找 本地程序集, 好比  「程序集名字.dll」,  若是找到,  直接加載到內存,

     若是找不到 本地程序集, 就找  ILBC 程序集,  好比  「程序集名字.ilb」,   若是找到,   先 JIT 編譯 爲 本地程序集, 再把 本地程序集 加載到內存 。

     若是  ILBC 程序集 也沒有找到,  就 報錯  「找不到 某某 程序集 。」  。

     

     怎麼把 本地程序集 加載到內存 ?    這 遵循 操做系統 提供的 方式,   好比   Windows  把  .dll 庫 加載到 應用程序 裏的 方式  。

 

總的來講,  加載程序集 的 流程 如上,  從 入口程序集 開始依次加載,  加載完成後,  調用 入口程序集 的  ILBC_Main()  開始 執行程序 。

另外, ILBC_GetMethodListList() 函數   應該是   ILBC_InitializeMethodListList() ,  具體 邏輯 不長, 但講起來煩瑣, 以後看 Demo 代碼就清楚了 。

 

     

能夠看到,  ILBC 運行時 加載 程序集 會 將 全部 引用到的 程序集 所有加載 完成,  纔會開始 執行程序 。

這是 和   .Net / C#  不一樣的 ,    .Net / C#  應該是 用到 這個 程序集 的時候 纔會 加載,  用到這個 程序集 是指 第一次 調用到 這個 程序集 裏的 類 的時候  。

 

實際上,   .Net / C#   的 動態加載 的 粒度 可能 更細,  多是  Class  這一級別 的,

咱們在 調試 .Net / C# 程序 的 時候 能夠 觀察到,  只有 第一次 用到 某個 Class 的 時候,  這個 Class 的 靜態構造函數 纔會被 調用  。

 

從這一點上來看,   .Net / C#  的 動態性 比   ILBC   更強,  更加動態  。

 

進一步,   ILBC 加載 的 單位 是 整個 程序集,  而不是 類(Class),  若是是 本地程序集, 則將  整個 本地程序集 加載到內存,

若是 是  ILBC 程序集, 則 對 整個 ILBC 程序集 進行  JIT 編譯,  編譯爲  本地程序集  後, 再把 整個 本地程序集  加載到內存  。

 

也所以,   D# / ILBC  不提供  類 的 靜態構造函數,   而是 提供一個    ILBC_AssemblyLoad()  函數,    ILBC 運行時 會在 加載 程序集 完成時 調用   ILBC_AssemblyLoad()  函數,  整個程序集 全部 類 的 初始化 工做 能夠在   ILBC_AssemblyLoad()   裏 來 完成  。

 

.Net / C#  的  動態性 須要 更加 複雜 的 設計 和 實現,   這不是    ILBC    的   定位 。

 

咱們能夠 探討 一下, 若是要實現 .Net / C#  的 動態性,  好比     第一次 new 類的對象   或者    第一次調用類的靜態方法   時,  加載類(若是 Assembly 未加載 則 先加載 Assembly 再加載 Class) 並 調用 類的靜態構造函數  這個 動態加載 怎麼實現:

咱們能夠寫一段 僞碼:

 

簡單起見,  咱們假設 Assembly 已經加載了,  只要 判斷 類 是否已加載, 若未加載 則 加載 類  。

 

編譯器 會 把 new 類 的 對象, 以及 調用 類的 靜態方法 的 代碼 處理成 一段 臨時代碼, 咱們稱之爲 「連接代碼」,

假設 該 類 是  A Class,

僞碼以下:

 

bool   ifAClassLoad   =   false;

if (  !   ifAClassLoad   )

{

            lock (  ifAClassLoad  )

            {

                         if (   !   ifAClassLoad    )

                         {                               

                                       加載 A Class    ;

                                       調用 類 的 靜態構造函數    ;

 

                                       ifAClassLoad    =    true  ;

 

                         }

            }

}

 

new ()    或者    A.靜態方法()       ;  

 

按照這個 代碼 的 邏輯,   第一次 new A()  或者  調用  A.靜態方法()  時, 會 判斷 A Class 是否已加載,  若是未加載, 會有一個 線程 通知 CLR 加載 A Class, 其它 線程 等待(若是 有 其它線程 也在 new A()  或者  調用  A.靜態方法()  的話),   CLR  加載完成後,   就執行 真正的 new A() 或者 A.靜態方法() ,

以後, 再  new A()   或者   調用 A.靜態方法()    的時候,  在  連接代碼  的 第一句,

 

if (  !   ifAClassLoad   )

 

就能夠 判斷 出來 A Class 已經加載,  因而就直接執行   new A()  或者  A.靜態方法()   。

 

但 這樣的 作法,  每次     new A()  或者  A.靜態方法()     都要有 一個 判斷,  雖然 只是一個 判斷, 但從 微觀 上來講, 也形成了 性能消耗  。

 

這樣的 性能消耗, 應該是  「應該被優化掉的」  。

 

若是  .Net / C#  已經 把 這個 判斷 優化掉了,  那麼 應該用到了  「修改已經編譯好的本地代碼」  的 操做, 形象的講, 就是給 「已經編譯好的本地代碼」 作了個 「微創手術」  。

具體就是 在 第一次 加載 成功後,  .Net CLR  會 把 這段  「連接代碼」  替換掉,  替換爲  new A()  和   A.靜態函數()   的 代碼,

在 新的    new A()   和   A.靜態函數()    代碼中,   A()  構造函數   和    A.靜態方法()    已經替換爲   A Class  加載後的 實際的 函數地址 。

這樣,  替換後的 代碼 和 訪問 同一個 程序集 中的 類 的  代碼 是 同樣的 。

性能 也和  訪問 同一個 程序集 中的 類 同樣 。

 

順便加一句,  原本 連接代碼 中  new A()  和  A.靜態函數()   的 部分 還有一個 相似 調用 虛函數 的 查函數表 的 操做,  也被這個 替換 優化掉了 。

 

這個 技術 很底層,  ILBC 不打算 涉及 這個 技術,

ILBC  仍然 把  C 語言 和  C 編譯器(InnerC)  看做一個總體,  不會  介入  C 編譯器 的 工做細節 。

 

不過,  從上面的討論也能夠知道,  若是   ILBC  想實現 和   .Net / C#   同樣的  「動態特性」,  好比 用到  A Class  的時候 才 加載  A Class,  若是 A Class 所在的 程序集 未加載 則 先加載程序集 再 加載 A Class,

若是要作到 這樣 的 動態特性 的話,  簡單點 也能夠用 上面的  「連接代碼」  的 作法,  只是每次調用  new A() 構造函數 和 A.靜態方法()  都要多一個

 

if (  !   ifAClassLoad   )

 

的 判斷 了 。

還有 就是 查函數表 的 操做 也是要有的 。

 

固然, 即便不實現這個 「動態特性」,  查函數表 的 操做 也是有的 。

ILBC  的 動態連接 就 至關於 調用 虛函數  。

 

不過 即便用了 上面  「連接代碼」  的 方式, 也只能 「用到某個 程序集 的 時候 才加載 程序集」, 還不能達到 Class 的 粒度,

由於 上文 也說了,    ILBC  是 把 整個  ILBC 程序集 編譯成  本地程序集 的,

這是由於   ILBC 程序集 是  C 語言 寫的,  C 語言 只能 整個項目(程序集) 一塊兒編譯, 不能把 裏面的  .c 文件 一個一個 拿出來編譯 。

就算能把   若干   .c 文件 任意 的 拿出來 編譯,  根據 ILBC 規範,  這些 單獨 拿出來的  .c 文件 編譯成的 程序集 裏 必需要 提供    ILBC_GetAssemblyList(), ILBC_InitializeMethodList(),   ILBC_Link()   函數,  這就亂套了 。  由於 本來的程序集 已經 爲 本來的整個項目 生成了 一份 這些 函數 。

假設 A 引用 B,  A 裏 編譯好的 邏輯 是 引用 B,  如今 把 B 拆成了 若干個 小程序集, 你讓 A 怎麼引用 ? 

相關文章
相關標籤/搜索