【譯】CLR類型載入器設計

来源:https://www.cnblogs.com/netry/archive/2022/09/20/clr-type-loader-chinese.html
-Advertisement-
Play Games

類型載入器設計(Type Loader Design) 原文:https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/type-loader.md 作者: Ladi Prosek - 2007 翻譯:幾秋 (https ...


類型載入器設計(Type Loader Design)

原文:https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/type-loader.md
作者: Ladi Prosek - 2007
翻譯:幾秋 (https://www.cnblogs.com/netry/)

介紹

在一個基於類的(class-based)的面向對象系統中,類型(type)是一個模板,它描述了單個實例將包含的數據、將提供的功能。如果不首先定義對象的類型,就不可能創建對象1。如果兩個對象是同一個類型的實例,就可以說它們是同一個類型;事實上(即使)兩個對象定義了完全相同的成員,它們可能也沒有任何關聯。

上面一段可以用來描述一個典型的C++系統。CLR必不可少的一個附加功能是完整的運行時類型信息的可用性。為了“管理”托管代碼並提供類型安全的環境,運行時必須在任何時候都要能知道任意對象的類型。這種類型信息必須是不用大量計算就可以很容易地得到,因為類型標識查詢被認為是相當頻繁的(例如,任何類型轉換都涉及到查詢對象的類型標識,以驗證轉換是安全並且可以執行的)。

此性能要求排除了所有的字典查找方法,我們只剩下以下架構
圖一
圖一 抽象巨集觀的對象設計

除了實際的實例數據之外,每個對象還包含一個類型id的指針, 指向表示該類型的結構。這個概念和C++虛表(v-table)指針相似,但是這個結構(我們現在稱為類型,後文會更精確地定義它),它包含的不僅僅是一個虛表,對於實例,它必須包含關於層次結構的信息(即繼承關係),以便能夠回答“is-a”的包含問題。

1C# 3.0引入的匿名類型,允許你不用顯示引用一個類型就可以定義對象 - 只需直接列出其欄位即可,不要讓這愚弄你,實際上編譯器在幕後為你創建了一個類型。

1.1 相關閱讀

[1] Martin Abadi, Luca Cardelli, A Theory of Objects, ISBN 978-0387947754

[2] Design and Implementation of Generics for the .NET Common Language Runtime

[3] ECMA Standard for the Common Language Infrastructure (CLI)

1.2 設計目標

類型載入器(type loader)有時也稱為類載入器(class loader,常見於各種Java八股文),這種說法嚴格來說是不正確的,因為類(class)只是類型(type)的子集 - 即引用類型,類型載入器也會載入值類型,它的最終目的是構建表示要求它載入的類型的數據結構。以下是載入器應具有的屬性:

  • 快速類型查找 (通過[module, token] 或者 [assembly, name] 查找).
  • 優化的記憶體佈局已實現良好的工作集大小、緩存命中率和 JIT編譯後的代碼性能。
  • 類型安全 - 不載入格式錯誤的類型,並拋出一個 TypeLoadException 異常。
  • 併發性 - 在多線程環境中具有良好的可擴展性。

2 類型載入器架構

載入器的入口點(entry-points,可以理解為公開的方法)數量相對較少。儘管每個入口點的簽名略有不同,但是它們都有相同的語義。它們採用以元數據 token或者name字元串為形式的類型/成員名稱,token的作用域(模塊或者程式集),以及一些附加信息如標誌;然後以句柄(handle)的形式返回已載入的實體。

在JIT過程中,通常會有調用很多次類型載入器。思考下麵的代碼:

object CreateClass()
{
    return new MyClass();
}

在它IL代碼里,MyClass被一個元數據token所引用。為了生成一個對 JIT_New(它是真正完成實例化的函數) 幫助方法的調用指令,JIT會要求類型載入器去載入這個類型並返回一個句柄。然後這個句柄將作為一個立即數(immediate value)直接嵌入到JIT編譯後的代碼中。類型和成員通常是在JIT過程中被解析和載入的,而不是在運行時階段,它還解釋了像這樣的代碼有時容易引起混淆的行為:

object CreateClass()
{
    try {
        return new MyClass();
    } catch (TypeLoadException) {
        return null;
    }
}

如果MyClass載入失敗,例如,因為它應該在另一個程式集中定義,並且在最新的版本中被意外刪除了,所以此代碼仍將拋出TypeLoadException。這裡異常不會被捕獲的原因是這段代碼根本沒有執行!這個異常發生在JIT的過程中,只能在調用了CreateClass並使它JIT完成的方法里被捕獲。此外,由於內聯(inlining)的存在,觸發JIT的時機有時並不是那麼明顯,因此用戶不應該期待和依賴於這種不確定的行為。

關鍵數據結構

CLR中最通用的類型名稱是TypeHandle,它是一個的抽象實體,封裝了指向一個MethodTable(表示“普通的”類型像System.Object 或者 List<string>)或者一個TypeDesc(表示 byref、指針、函數指針、數組,以及泛型變數)的指針。它構成了一個類型的標識,因為當且僅當兩個句柄表示同一類型時,它們才是相等的。為了節省空間, TypeHandle通過設置指針的第二低位為 1(即 (ptr|2))來表示它指向的TypeDesc,而不是用額外的標誌。TypeDesc是“抽象的”,並且有如下的繼承體系:

圖2

圖2 TypeDesc體系

TypeDesc

抽象類型描述符。具體的描述符類型由標誌確定。

TypeVarTypeDesc

表示一個類型變數,即 List<T>或者Array.Sort<T>中的T(參見下文關於泛型的部分)。類型變數不會在多個類型或者方法間共用,因此每個變數有且只有一個所有者。

FnPtrTypeDesc

表示一個函數指針,實質上是一個引用返回類型和參數的變長type handle列表,這個描述符不太常見,因為C#不支持函數指針。然後托管C++會使用它們。

ParamTypeDesc

這個描述符表示一個byref和指針類型,byref是refout這兩個C#關鍵字應用到方法參數3的結果,而指針類型是非托管的指針,指向unsafe C#和托管C++中使用的數據。

ArrayTypeDesc

表示數組類型. 派生自ParamTypeDesc,因為數組也由單個參數(其元素的類型)參數化。這與參數數量可變的泛型實例化相反。

MethodTable

這是目前為止運行時的最重要的數據結構,它表示所有不屬於上述類別的類型(它包括基本類型,開放(open)或閉合(closed)的泛型類型)。它包含了所有關於類型需要快速查找的信息,像它的父類型,實現的介面,和虛表。

EEClass

為了提高工作集和緩存利用率,MethodTable 的數據被分為“熱”和“冷”兩種結構。MethodTable 本身只存儲程式在穩定狀態(steady state)下所需的“熱”數據;而EEClass存儲通常只在類型載入、JIT 編譯或者反射中需要的“冷”數據。每個MethodTable 指向一個 EEClass.

此外,EEClass是被泛型共用的,多個泛型MethodTable可以指向同一個EEClass。這種共用對可以存儲在 EEClass 上的數據增加了額外的約束。

MethodDesc

顧名思義,此結構用來描述方法。它實際上有幾種變體,它們有相應的 MethodDesc子類型,但是大多數都超出了本文的討論範圍。這裡只需說其中一個叫做InstantiatedMethodDesc的子類型,它在泛型中扮演了重要角色。更多信息請參考Method Descriptor Design

FieldDesc

MethodDesc相似 , 此結構用來描述欄位。除了某些 COM 互操作場景,EE(EEClass中的EE,即Execution Engine,執行引擎)根本不在乎屬性和事件,因為它們最終歸結為方法和欄位,只有編譯器和反射才能生成和理解它們,以便提供語法糖之類的體驗。

2這對調試很有用,如果一個TypeHandle的值是以2, 6, A, 或者E結尾,那麼它就是不是MethodTable,為了成功地觀察到TypeDesc,必須清除額外的位。

3註意refout之間的區別僅在於參數屬性,就類型系統而言,它們都是相同的類型。

2.1 載入級別

當類型載入器被要求載入一個指定的類型時,例如通過一個typedef/typeref/typespec的token和一個Module,它不會一次性做完所有的工作,而是分階段完成載入;這是因為一個類型經常會依賴其它的類型,如果在能被其它類型引用之前就完全載入,將導致無限遞歸和死鎖,思考下麵的代碼:

class A<T> : C<B<T>>
{ }

class B<T> : C<A<T>>
{ }

class C<T>
{ }

上面的類型都是有效的,顯然 AB相互依賴。

載入器首先會創建表示這個類型的一些結構,然後使用無需載入其它類型就可得到的數據來初始化它們。當“沒有依賴”的工作完成,這些結構就可以被其它地方所引用,通常是通過將指向它們的指針粘貼到其它結構中。之後,載入器以增量步驟進行,用越來越多的信息填充這些結構,直到類型完全載入完成。在上面的例子中,首先AB的基類會近似於不包括其它類型,然後才會被真正的類型所替代。

所謂的載入載入級別,就是用來描述這些半載入狀態( half-loaded),從 CLASS_LOAD_BEGIN開始,到CLASS_LOADED結束,中間還有一些中間級別。在 classloadlevel.h源文件里,每個級別都有豐富且有用的註釋。註意,雖然類型可以保存到NGEN鏡像中,但表示的結構不能簡單地映射或者寫入記憶體,然後不做額外“恢復”工作就使用。一個類型是來自於NGEN鏡像並且需要“恢復”,這一信息它的載入級別也可以感知到。

更多關於載入級別的解釋請看Design and Implementation of Generics for the .NET Common Language Runtime

2.2 泛型

在沒有泛型的世界里,一切都很美好,每個人都很開心,因為每一個普通類型(不是由 TypeDesc 所表示的類型)都有一個 MethodTable指向他關聯的EEClass,這個EEClass又指回它的MethodTable,該類型的所有實例都包含一個指向MethodTable,作為偏移量為0處的第一個欄位,即在被視為參考值的地址上。為了節省空間,由該類型聲明的MethodDesc表示方法,被組織在EEClass指向的塊鏈表中4

圖3

圖3 具有非泛型方法的非泛型類型

4當然,當托管代碼運行時,它不會通過在這些塊中查找方法來調用它們,調用一個方法是很“熱”的操作,正常只需要訪問MethodTable中的信息。

2.2.1 術語

泛型形式參數(Generic Parameter)

一個能被其它類型替換的占位符;如 List<T>中的T。有時也稱作形式類型參數(formal type parameter)。一個泛型形式參數有一個名字和一個可選的泛型約束。

泛型實際參數(Generic Argument)

一個替換泛型形式參數的具體類型;如List<int>中的int。註意,一個泛型形式參數也可以被用作泛型實際參數。思考下麵的代碼:

List<T> GetList<T>()
{
    return new List<T>();
}

這個方法有一個泛型形式參數T,被用作泛型列表的泛型實際參數。

泛型約束

泛型形式參數對其潛在的泛型實際參數的可選要求。不滿足要求的類型不能替換形式參數,這是類型載入器強制的。有下麵三種泛型約束:

  1. 特殊約束

    • 引用類型約束 —— 泛型實際參數必須是一個引用類型(相對應的是一個值類型)。在C#里,用class表示這個約束。
    public class A<T> where T : class
    
    • 值類型約束 —— 泛型實際參數必須是一個除System.Nullable<T>之外的值類型。C#里使用struct這個關鍵字。
    public class A<T> where T : struct
    
    • 預設構造函數約束 —— 泛型實際參數必須有個公開的無參構造函數。C#里用new()表示。
    public class A<T> where T : new()
    
  2. 基類約束 —— 泛型實際參數必須派生自(或者直接就是)給定的非介面類型。顯然最多只能有一個引用類型作為基類約束。

     public class A<T> where T : EventArgs
    
  3. 介面實現約束 —— 泛型實際參數必須實現(或者直接就是)給定的介面類型。可以同時有多個介面約束。

     public class A<T> where T : ICloneable, IComparable<T>
    

上面的約束可以被一個顯式AND組合起來,即一個泛型形式參數可以約束要派生自一個給定的類,實現幾個介面,並且還要有預設的構造函數。聲明類型的所有泛型參數都可以用來表示約束,從而在參數之間引入相互依賴關係,例如:

public class A<S, T, U>
	where S : T
	where T : IList<U> {
    void f<V>(V v) where V : S {}
}

實例(Instantiation)

一組泛型實際參數,用來替換泛型類型或者方法中的泛型形式參數。每一個載入的泛型和方法都有它的實例。

典型實例(Typical Instantiation)

一個實例僅僅包含類型或者方法自己的類型參數,且和聲明參數一樣的順序。每個泛型類型和方法只存在一個典型實例。通常當我們提到開放泛型類型(Open generic type)時,就是指它的典型實例,例如:

public class A<S, T, U> {}

C#會把typeof(A<,,>)編譯為一個ldtoken A\'3,讓運行時載入S , T , U實例化的 A`3

規範實例(Canonical Instantiation)

一個所有泛型實際參數都是System.__Canon的實例。System.__Canon是一個定義在mscorlib中的內部類型,其任務只是為了作為規範,並且與其它可能用作泛型實際參數的類型不同。帶有規範實例的類型/方法被用作所有實例的代表,並攜帶所有實例共用的信息。由於System.__Canon顯然不能滿足泛型形參上可能攜帶的任何約束,因此約束檢查對於System.__Canon是特例,會忽略了這些行為。

2.2.2 共用

隨著泛型的出現,運行時載入的類型數量變得更多了,雖然不同實例的泛型(如List<string> and List<object>)是不同的類型,它們都有自己的MethodTable,事實證明,他們有大量信息可以共用。這種共用對記憶體足跡(memory footprint)有積極的影響,因此也會提高性能。

圖4
圖4 帶有非泛型方法的泛型類型 - 共用EEClass

當前所有包含引用類型的實例都共用同一個EEClass 和 它的 MethodDesc。這是可行的,因為所有的引用類型大小都一樣 —— 4或者8個位元組。因此所有這些類型的佈局都是相同的。上圖為List<object>List<string>闡明瞭這點。規範的 MethodTable在第一個引用類型實例被載入之前就自動創建,它包含了熱數據,但不是特定於像非虛的方法槽(non-virtual slots)或者RemotableMethodInfo實例。僅包含值類型的實例是不共用的,並且每個這種實例化的類型都有自己的未共用EEClass

目前為止已載入泛型類型的MethodTable,會被緩存到一個屬於它們載入器模塊(loader module)的哈希表中5。在一個新的實例構造之前,首先會查詢這個哈希表,確保不會有兩個多種多個 MethodTable實例表示同一個類型。

更多關於泛型共用的信息請看Design and Implementation of Generics for the .NET Common Language Runtime

5從NGEN鏡像載入的類型,事情會變得有點複雜。

後記

本文翻譯自BotR中的一篇,可以幫助我們瞭解CLR的類型載入機制(註意是Type類型,而不是Class類),文中涉及到術語或者容易混淆的地方,我有在隨後的括弧里列出原文和解釋。如有翻譯不正確的地方,歡迎指正。
文章內容偏底層,有很多瑣碎的概念,後面有機會我會一一寫文章介紹。

作者: 幾秋

出處: https://www.cnblogs.com/netry/p/clr-type-loader-chinese.html

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 來源:liuchenyang0515.blog.csdn.net/article/details/109263510 對稱加密 兩邊用同一個密鑰來加解密。 A把明文通過某一演算法加密之後得到密文,然後把密文發送給B,B接收到密文之後用相同的密鑰執行相同的演算法去解密。X沒有密鑰,即使竊取到密文也無法竊聽 ...
  • 多用戶即時通訊系統01 1.項目開發流程 2.需求分析 用戶登錄 拉取線上用戶列表 無異常退出(包括客戶端和服務端) 私聊 群聊 發文件 伺服器推送新聞/廣播 3.設計階段 3.1界面設計 用戶登錄: 拉取線上用戶列表: 私聊: 群聊: 發文件: 文件伺服器推送新聞: 3.2通訊系統整體設計 總結: ...
  • 實時展示用戶上傳的頭像 總體思路 """ 1.首先需要給對應的上傳頭像input框綁定一個文本域變化事件 (當檢測到用戶對該文件框上傳了頭像就會觸發一系列操作) 2.再生成一個文件閱讀器對象 3.再獲取用戶上傳的文件頭像 4.把用戶上傳的文件頭像交給文件閱讀器對象FileReader讀取 5.利用文 ...
  • 1 垃圾收集三件事 哪些記憶體需要回收:死去的對象需要回收 什麼時候回收 如何回收 按照jvm記憶體區域劃分原則:程式計數器、虛擬機棧、本地方法棧3個區域的記憶體隨線程創建而劃分,因此線程結束時,記憶體也自動釋放。 本章節分析的是Java堆和方法區的記憶體管理策略 1、虛擬機棧、本地方法棧,棧中的棧幀隨著方法 ...
  • Python中,要想知道一個字元串有多少個字元(獲得字元串長度),或者一個字元串占用多少個位元組,可以使用len()函數。 語法格式: len(string) string 用於指定要進行長度統計的字元串 示例: a = 'www.baidu.com' print(len(a)) 輸出 13 在 Py ...
  • 二、散點圖 import seaborn as sns import matplotlib.pyplot as plt sns.set_theme(style = 'whitegrid') # 載入 diamonds 數據集 diamonds = sns.load_dataset('diamonds ...
  • 能對比測試 為了直觀地感受 Disruptor 有多快,設計了一個性能對比測試:Producer 發佈 1 億次事件,從發佈第一個事件開始計時,捕捉 Consumer 處理完所有事件的耗時。 測試用例在 Producer 如何將事件通知到 Consumer 的實現方式上,設計了兩種不同的實現: Pr ...
  • 學習網站: http://seaborn.pydata.org/examples/scatterplot_matrix.html 一、Anscombe's quartet(安斯庫姆四重奏) 1973年,統計學家F.J. Anscombe構造出了四組奇特的數據。它告訴人們,數據分析之前,描繪數據所對應 ...
一周排行
    -Advertisement-
    Play Games
  • 一:背景 1.講故事 在分析的眾多dump中,經常會遇到各種奇葩的問題,僅通過dump這種快照形式還是有很多問題搞不定,而通過 perfview 這種粒度又太粗,很難找到問題之所在,真的很頭疼,比如本篇的 短命線程 問題,參考圖如下: 我們在 t2 時刻抓取的dump對查看 短命線程 毫無幫助,我根 ...
  • 在日常後端Api開發中,我們跟前端的溝通中,通常需要協商好入參的數據類型,和參數是通過什麼方式存在於請求中的,是表單(form)、請求體(body)、地址欄參數(query)、還是說通過請求頭(header)。 當協商好後,我們的介面又需要怎麼去接收這些數據呢?很多小伙伴可能上手就是直接寫一個實體, ...
  • 許多情況下我們需要用到攝像頭獲取圖像,進而處理圖像,這篇博文介紹利用pyqt5、OpenCV實現用電腦上連接的攝像頭拍照並保存照片。為了使用和後續開發方便,這裡利用pyqt5設計了個相機界面,後面將介紹如何實現,要點包括界面設計、邏輯實現及完整代碼。 ...
  • 思路分析 註冊頁面需要對用戶提交的數據進行校驗,並且需要對用戶輸入錯誤的地方進行提示! 所有我們需要使用forms組件搭建註冊頁面! 平時我們書寫form是組件的時候是在views.py裡面書寫的, 但是為了接耦合,我們需要將forms組件都單獨寫在一個地方,需要用的時候導入就行! 例如,在項目文件 ...
  • 思路分析 登錄頁面,我們還是採用ajax的方式提交用戶數據 唯一需要學習的是如何製作圖片驗證碼! 具體的登錄頁面效果圖如下: 如何製作圖片驗證碼 推導步驟1:在img標簽的src屬性里放上驗證碼的請求路徑 補充1.img的src屬性: 1.圖片路徑 2.url 3.圖片的二進位數據 補充2:字體樣式 ...
  • 哈嘍,兄弟們! 最近有許多小伙伴都在吐槽打工好難。 每天都是執行許多重覆的任務 例如閱讀新聞、發郵件、查看天氣、打開書簽、清理文件夾等等, 使用自動化腳本,就無需手動一次又一次地完成這些任務, 非常方便啊有木有?! 而在某種程度上,Python 就是自動化的代名詞。 今天就來和大家一起學習一下, 用 ...
  • 作者:IT王小二 博客:https://itwxe.com 前面小二介紹過使用Typora+PicGo+LskyPro打造舒適寫作環境,那時候需要使用水印功能,但是小二在升級LskyPro2.x版本發現有很多不如人意的東西,遂棄用LskyPro使用MinIO結合代碼實現自己需要的圖床功能,也適合以後 ...
  • OpenAI Gym是一款用於研發和比較強化學習演算法的工具包,本文主要介紹Gym模擬環境的功能和工具包的使用方法,並詳細介紹其中的經典控制問題中的倒立擺(CartPole-v0/1)問題。最後針對倒立擺問題如何建立控制模型並採用爬山演算法優化進行了介紹,並給出了相應的完整python代碼示例和解釋。要... ...
  • python爬蟲瀏覽器偽裝 #導入urllib.request模塊 import urllib.request #設置請求頭 headers=("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, l ...
  • 前端代碼搭建 主要利用的是bootstrap3中js插件里的模態框版塊 <li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密碼</a></li> <div class="modal fade bs-exam ...