【BotR】CLR型別系統

2022-09-23 21:00:21

.NET執行時之書(Book of the Runtime,簡稱BotR)是一系列描述.NET執行時的檔案,2007年左右在微軟內部建立,最初目的是為了幫助其新員工快速上手.NET執行時;隨著.NET開源,BotR也被公開了出來,如果想深入理解CLR,這系列文章不可錯過。

BotR系列目錄:
[1] CLR型別載入器設計(Type Loader Design)
[2] CLR型別系統概述(Type System Overview)

型別系統概述(Type System Overview)

原文:https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/type-system.md
作者: David Wrighton - 2010
翻譯:幾秋 (https://www.cnblogs.com/netry/)

介紹

CLR型別系統是我們在ECMA規範+擴充套件中描述的型別系統的表示形式。

概述

該型別系統是由一系列資料結構(其中一些在BotR的其它章節有描述)和操作這些資料結構的演演算法組合而成,它不是通過反射暴露出來的型別系統,儘管反射確實依賴於這個系統。

由型別系統維護的主要資料結構是:

  • MethodTable
  • EEClass
  • MethodDesc
  • FieldDesc
  • TypeDesc
  • ClassLoader

由型別系統包含的主要演演算法是:

  • Type Loader: 用於載入型別並建立型別系統中大部分的重要資料結構。
  • CanCastTo and similar: 型別比較功能。
  • LoadTypeHandle: 主要用於查詢型別。
  • Signature parsing: 用於比較和收集有關方法和欄位的資訊
  • GetMethod/FieldDesc: 用於查詢、載入方法和欄位。
  • Virtual Stub Dispatch: 用於查詢對介面的虛呼叫的存根。

還有很多輔助資料結構和演演算法為CLR的其餘部分提供各種資訊,但它們對於整個系統的理解並不那麼重要。

元件架構

型別系統的資料結構通常被各種演演算法所使用。本檔案不會涉及型別系統演演算法(),但是它會試圖描述各種主要資料結構。

依賴

型別系統大體上是給CLR中很多部分提供服務,多數核心元件都對型別系統的行為有某種形式的依賴性。下圖描述了影響型別系統的通用資料流,它不是很全面,但是指出了只要的資訊流。

元件依賴

型別系統的主要依賴關係如下:

  • 載入器工作需要獲取正確的後設資料。
  • 後設資料系統(metadata system)提供一個後設資料API去收集資訊。
  • 安全系統(security system)告知型別系統是否允許某些型別系統結構。
  • 應用程式域(AppDomain)提供一個LoaderAllocator去處理型別系統資料結構的分配行為。

依賴於此元件的元件

型別系統有3個主要元件依賴於它:

  • jit介面和jit helpers主要依賴於型別、方法,和欄位搜尋功能。一旦找到了型別系統物件,返回的資料結構就會進行裁剪,以提供jit所需的資訊。
  • 反射使用型別系統提供對ECMA標準化概念相對簡單的存取,CLR型別系統的資料結構中正好有這些。
  • 常規受控程式碼執行 需要使用型別系統進行型別比較邏輯和虛擬存根分派

型別系統設計

核心型別系統資料結構是表示實際載入型別的資料結構(例如,TypeHandle, MethodTable, MethodDesc, TypeDesc, EEClass)和允許在載入型別後找到它們的資料結構(例如,ClassLoader, Assembly, Module, RIDMaps)。

載入型別的資料結構和演演算法在BotR的Type LoaderMethodDesc章節中有討論。

將這些資料結構繫結在一起是一組功能, 允許JIT/Reflection/TypeLoader/stackwalker去查詢現存型別和方法,一般的想法是,這些搜尋應該很容易地由ECMA CLI規範中指定的後設資料令牌/簽名(metadata tokens/signatures)驅動。

最後,當合適的型別系統資料結構被找到,我們有演演算法從型別中收集資訊,有and/or比較兩個型別。可以在 Virtual Stub Dispatch找到這種演演算法的一個特別複雜的例子。

設計目標和非目標

目標

  • 存取執行時執行(非反射)程式碼所需的資訊要非常快。
  • 存取編譯時生成程式碼所需的資訊要是非常直截了當的。
  • 垃圾回收器(arbage collector)、堆疊遍歷器(stackwalker,沒找到準確的中文翻譯,詳情可參考這篇文章 Stackwalking in the .NET Runtime/)無需鎖或分配記憶體就可以方法資訊。
  • 一次只載入最少量的型別。
  • 型別載入時只載入最少需要載入的型別。
  • 型別系統的資料結構必須儲存在NGEN映象中。

非目標

  • 後設資料中的所有資訊都直接反映在CLR的資料結構中。
  • 所有的反射操作都要非常快。

執行時在受控程式碼執行期間使用的一個典型演演算法設計

型別轉換演演算法(casting algorithm)是型別系統中的典型演演算法,在受控程式碼的執行過程中大量使用這種演演算法。

這個演演算法至少有4個獨立的入口(entry point),選擇每個入口都是為了提供不同的快速路徑,希望能夠實現最佳的效能。

  • 一個物件能否被轉換成一個非型別等價的非陣列型別(non-type equivalent non-array type)?
  • 一個物件能否被轉換成一個沒有實現泛型變體(generic variance)的介面型別?
  • 一個物件能否被轉換成一個陣列型別?
  • 一個型別的物件能否被轉換成轉換為任意其他託管型別?

除了最後一個之外,每個實現都進行了優化,以便在不完全通用的情況下提高效能。例如,「一個型別能否被轉換成一個父類別?」就是 「一個物件能否被轉換成一個非型別等價的非陣列型別」的變體,它通過單迴圈遍歷一個單連結串列實現。這隻能搜尋可能的轉換操作的一個子集,但可以通過檢查試圖強制轉換的型別來確定是否是合適的集合,這個演演算法在jit helper JIT_ChkCastClass_Portable中實現。

假設:

  • 演演算法的特殊用途實現通常是效能改進。
  • 額外版本的演演算法不會提供無法克服的維護問題(insurmountable maintenance problem)。

型別系統中典型搜尋演演算法設計

在型別系統中很多演演算法遵循這種常見模式。型別系統通常用於查詢型別,這可以通過任意數量的輸入觸發,如JIT、反射、序列化、遠端呼叫等等。在這些情況下,對型別系統的基本輸入是:

  • 來自開始搜尋的上下文(一個模組或者程式集指標)。
  • 在初始上下文中描述所需型別的識別符號(identifier),通常是一個令牌(token),或者一個字串(如果搜尋上下文是一個程式集)。

這個演演算法必須首先解碼識別符號。對於搜尋一個型別的場景,令牌可能是typedef令牌、typeref令牌、typespec令牌,或者是一個字串。這些不同種類的識別符號都會將導致不同形式的查詢。

  • 一個typedef令牌將導致在模組的RidMap中進行查詢。這是一個簡單的陣列索引。
  • 一個typeref令牌將導致查詢此令牌所參照的程式集,然後用找到的組合指標和從typeref表中收集的字串重新開始型別查詢演演算法。
  • 一個typespec令牌指示必須對簽名(signature)進行解析才能找到簽名。解析簽名以查詢載入該型別所需的資訊,這將遞迴的觸發更多型別查詢。
  • 名稱(name)用於在程式集之間進行繫結。TypeDef/ExportedTypes表用來做匹配搜尋。主要:此搜尋通過清單模組物件(manifest module object)上的雜湊表進行優化。

從這個設計中可以明顯看出一些搜尋演演算法在型別系統中的共同特點:

  • 搜尋使用的輸入是和後設資料加密耦合的。特別是後設資料令牌和字串名稱,它們檢查被傳來傳去,還有,這些搜尋是和模組綁在一起的,它們直接對映到 .dll和.exe檔案。
  • 使用快取的資訊去提高效能。RidMap和雜湊表是經過優化的資料結構,用於改進這些查詢。
  • 這些演演算法通常根據其輸入有3-4個不同的路徑。

除了這個總體設計,在此基礎上,還有一些額外的需求:

  • 假設 在GC停止時搜尋已載入的型別是安全的。
  • 不變性 一個已經載入了的型別將總是可以被搜尋到。
  • 問題搜尋程式依賴於後設資料讀取。某些場景可能會導致效能不足。

此搜尋演演算法是 JIT期間使用的典型程式。它具有許多共同的特徵:

  • 它使用後設資料
  • 它需要在許多地方查詢資料
  • 在我們的資料結構中有相對少量的重複資料
  • 它通常不會深度遞迴,也沒有迴圈

這使我們能夠滿足效能要求,以及使用基於IL的JIT所必需的特徵。

型別系統對垃圾回收器的要求

垃圾回收器要有關型別範例分配在GC堆上的資訊,這是通過一個指向型別系統資料結構(MethodTable)的指標來完成,該MethodTable位於每一個託管物件的開頭。附著到這個MethodTable之上的,是一個描述型別範例GC佈局的資料結構。該佈局有兩種形式(一般型別和物件陣列是一種,值型別陣列是另一種)。

  • 假設:型別系統的資料結構有一個生命週期,它超過了自身描述型別的託管物件的生命週期。
  • 要求: 垃圾回收器需要在執行時掛起時執行堆疊遍歷器(stack walker),這將在下面討論。

Stackwalker對型別系統的要求

堆疊遍歷器/GC堆疊遍歷器在兩種情況下要求型別系統輸入:

  • 用於查詢值型別在堆疊上的大小
  • 用於查詢要在堆疊上的值型別內報告的GC根

由於各種原因,包括希望延遲載入型別,以及避免生成多個版本的程式碼(僅通過相關的gc資訊不同),CLR當前需要遍歷堆疊上的方法簽名,這種需求很少得到滿足,因為它要求在特定的時刻執行堆疊遍歷,但是為了滿足我們的現實目標,當遍歷堆疊的時候,必須能夠遍歷簽名。

堆疊遍歷器以大約 3 種模式執行:

  • 因為安全或者例外處理的原因,要遍歷當前執行緒的堆疊
  • 因為GC,要遍歷使用執行緒的堆疊(所有執行緒都被EE掛起)
  • 使用分析工具(profiler)需要遍歷指定的執行緒(指定執行緒被掛起)

在GC和分析工具遍歷堆疊的情況,由於執行緒被掛起,分配記憶體或佔用大多數鎖是不安全的。這導致我們開發了一條通過型別系統的路徑,可以依賴它來遵循上述要求。型系統實現此目標所需的規則是:

  • 如果一個方法已經被呼叫,那麼被呼叫方法的所有值型別引數都將被載入到程序中的某個應用程式域中。
  • 從帶有簽名的程式集到實現該型別的程式集,參照它們的程式集必須在遍歷簽名之前被解析,這是遍歷堆疊中必要的一部分。

這是通過一系列廣泛而複雜的強制措施來實施的,包括型別載入器、NGEN映象生成過程和JIT。

  • 問題: 堆疊遍歷器對型別系統的要求是非常脆弱的。
  • 問題: 在型別系統上實現堆疊遍歷器的要求,需要侵入型別系統中的每個搜尋型別時可能接觸到的函數
  • 問題: 執行的簽名遍歷是使用正常的簽名遍歷程式碼完成的。此程式碼設計是在遍歷簽名時載入型別,但在這種情況下,型別載入功能是在假設實際上不會觸發任何型別載入的情況下使用的。
  • 問題: 堆疊遍歷器不僅需要型別系統的支援,還需要程式集載入器的支援,要滿足型別系統的需要,載入器還有很多問題。

型別系統與NGEN

型別系統資料結構是儲存到NGEN映象中的核心部分,不幸的是,這些資料結構邏輯內有指向其它NGEN映象的指標。為了處理這種情況,型別系統資料結構實現了一個稱為恢復(restoration)的概念。
在恢復期,當第一次需要型別系統資料結構時,該資料結構用正確的指標固定, 這與型別載入級別有關,請看前篇CLR型別載入器設計

還存在一個預恢復(pre-restored)資料結構的概念,這意味著資料結構在NGEN映象載入時足夠正確(在intra-module指標和預先載入型別修正之後),資料結構可以按原樣使用。此優化要求將NGEN映象「硬繫結」("hard bound")到其依賴程式集,詳情請檢視NGEN相關檔案。

型別系統和域中性載入(Domain Neutral Loading)

型別系統是實現域中性載入的核心部分,它通過在AppDomain建立時啟用LoaderOptimization選項暴露給使用者。Mscorlib始終作為域中性載入,此功能的核心要求是型別系統資料結構不能要求指向特定域狀態(domain specific state)的指標,這主要表現在圍繞靜態欄位和類建構函式的需求中。特別是,由於這個原因,一個類別建構函式是否已經執行不是核心MethodTable資料結構的一部分。並且有一種機制來儲存附加到DomainFile資料結構而不是MethodTable資料結構。

物理結構

型別系統的主要部分位於:

  • Class.cpp/inl/h – EEClass函數, 和BuildMethodTable
  • MethodTable.cpp/inl/h – 操作methodtable的函數
  • TypeDesc.cpp/inl/h – 檢查TypeDesc的函數
  • MetaSig.cpp SigParser – 簽名程式碼
  • FieldDesc /MethodDesc –檢查這些資料結構的函數
  • Generics – 泛型特定邏輯
  • Array – 處理陣列處理所需的特殊情況的程式碼
  • VirtualStubDispatch.cpp/h/inl – 虛擬存根分派(VSD)程式碼
  • VirtualCallStubCpu.hpp – 用於虛擬存根分派的處理器特定程式碼

主要入口函數是BuildMethodTable、 LoadTypeHandleThrowing、CanCastTo*、 GetMethodDescFromMemberDefOrRefOrSpecThrowing、 GetFieldDescFromMemberRefThrowing、 CompareSigs和 VirtualCallStubManager::ResolveWorkerStatic.

相關閱讀