數組和CLR-很是特殊的關係



數組和CLR-很是特殊的關係

原文地址:https://mattwarren.org/2017/05/08/Arrays-and-the-CLR-a-Very-Special-Relationship/
譯文做者:傑哥很忙git

前段時間,我寫了關於字符串和CLR之間的"特殊關係",事實證實,Array 和 CLR 具備更深的關係。程序員

順便說一下,若是你喜歡閱讀CLR本質類的文章,你可能會對這些文章頗有趣:github

公共語言運行時(CLR)的基礎

數組是 CLR 的基本部分,它們包含在 ECMA 規範中,以明確運行時必須實現數組:web

20200225233847.png

另外,還有一些專門處理數組的IL(中間語言)指令:算法

  • newarr <etype>
    建立一個元素類型爲etype的數組
  • ldelem.ref
    將位於指定數組索引處的包含對象引用的元素做爲 O 類型(對象引用)加載到計算堆棧的頂部,O類型與在 CIL 堆棧上推送的數組的元素類型相同。
  • stelem <typeTok>
    堆棧上的值保存到數組索引位置(stelem.istelem.i1stelem.i2stelem.r4 等相似)
  • ldlen
    將數組長度推到堆棧(本機無符號int類型)

有專用IL指令很是有意義,由於數組是不少其餘數據類型的構建基塊,你會你但願它能在 C# 等現代高級語言中可用、定義良好且高效。若是沒有數組,就不可能有列表、字典、隊列、堆棧、樹等數據結構,它們都是構建在數組之上的,這些數組以類型安全的方式提供對連續內存片斷的低級訪問。編程

內存和類型安全

這種內存類型安全很重要,由於沒有它,.NET就不能被描述爲"託管運行時",而且當你以更低級的語言編寫代碼時,你必須本身處理所遇到的類型安全的問題。c#

更具體地說,CLR 在使用數組時提供如下保護(來自BORT的"CLR 簡介"頁面中有關內存和類型安全部分):windows

雖然 GC 須要確保內存安全,可是還不夠。GC並不會阻止程序從數組的末尾索引或訪問對象末尾的字段(若是使用基地址和偏移量計算字段的地址,則可能有可能發生上述狀況)。可是,若是咱們確實防止上述這些狀況發生,那麼就會使程序員沒法建立內存不安全的程序。
雖然公共中間語言 (CIL) 確實有能夠獲取和設置任意內存的運算符(從而違反內存安全性),但它也具備如下內存安全運算符,CLR 強烈鼓勵在編程中使用它們:api

  1. 經過名稱提取(讀取)設置字段地址的字段提取運算符(LDFLD、STFLD、LDFLDA)。
  2. 按索引提取、設置和獲取數組元素地址的數組提取運算符(LDELEM、STELEM、LDELEMA)全部數組都包含一個標記,指定他們的長度。這使得在每次訪問以前進行自動邊界檢查。

譯者補充:BORT是Book Of The Runtime的縮寫。
對於完整的IL指令能夠點擊這裏查看
《託管數組結構》一文中有介紹數組的佈局,在類型句柄以後是數組長度。

此外,從BOTR頁的可驗證代碼-強制內存和類型安全"一節中提到:

事實上,須要運行時檢查的數量其實是很是小的。它們包括如下操做:

  1. 將指向基類型的指針轉換爲爲指向派生類型的指針(能夠靜態檢查相反的方向)
  2. 數組邊界檢查(正如咱們看到的內存安全性同樣)
  3. 指向新(指針)值的指針數組中分配元素。由於CLR數組有自由轉換規則,才須要此特定的檢查(稍後將對此進行更多介紹...)

可是,並不能免費得到類型安全保護,須要有一些性能開銷:

須要注意的是,須要執行這些檢查會對運行時進行要求。特別是:

  1. GC堆中的全部內存必須標記類型(以即可以實現強制轉換運算符)。在運行時類型信息必須能夠被獲取到,而且內容必須足夠豐富,以肯定強制轉換是否有效(例如,運行時須要知道繼承層次結構)。實際上,GC 堆上每一個對象中的第一個字段指向表示其類型的運行時數據結構。
  2. 全部數組還必須有大小字段(用於邊界檢查)。
  3. 數組必須包含有關其元素類型的完整類型信息

譯者補充:在《託管數組結構》一文中介紹了託管數組的佈局結構。第一個字段指向其類型的運行時數據結構指的是類型句柄,指向的是該對象的方法表。

實現細節

事實證實,數組大部分的內部實現最好描述爲「魔術」,Stack Overflow 的來自 Marc Gravell的回答很好的總結了它。

數組基本上是使用了「魔術」。由於它們早於泛型,但必須容許動態類型建立(即便在.NET 1.0中也須要),所以它們經過使用一些技巧、黑客等手段實現。

是的,數組在泛型存在以前就被參數化(即通用化)。這意味着你能夠在編寫 List<int>List<string> 以前建立數組(如 int[]string[]),這僅在 .NET 2.0 中成爲可能。

特殊幫助器類

全部這些魔術或技巧可能形成兩件事:

  • CLR 違反了全部常見的類型安全規則
  • 特殊的數組幫助器類叫作 SZArrayHelper

但首先,爲何須要這些技巧?來自《.NET Arrays, IList , Generic Algorithms, and what about STL?》

當咱們設計泛型集合類時,困擾個人一件事就是如何編寫一個通用算法,能處理數組和集合。固然,爲了驅動泛型編程,咱們必須儘量使數組和泛型集合無縫銜接。應該有一個簡單的解決方案來解決這個問題,這意味着你必編寫相同的代碼兩次,一次使用IList<T>,一次使用T[] 我忽然意識到的解決方案是數組須要實現咱們的通用 IList。咱們在 .Net Framework 1.1 中使數組實現了非泛型 IList,因爲缺乏 IList 強類型和全部數組(System.Array)的基類,因此實現至關簡單。咱們須要的是以強類型方式爲 IList<T> 執行相同的操做。

但它只針對常見狀況(即"單維"數組):

不過,這裏存在一些限制,咱們不想支持多維數組,由於 IList<T> 只提供單維訪問。 此外,具備非零下限的數組至關奇怪,而且可能不能很好的與 IList<T> 相匹配,由於大多數人可能會從 IList 的0到 Count 進行遍歷。所以,咱們不是使 System.Array 實現 IList<T>,使 T[] 實現 IList<T> 在這裏,T[] 表示以 0 爲下限的單維數組(一般在內部稱爲 SZArray,但我認爲 Brad 但願在某個時間點公開推廣術語"矢量"),而且元素類型爲 T。所以,Int32[]實現IList<Int32>string[]實現了IList<String>

此外,數組源代碼中的此註釋進一步闡明瞭緣由:

//----------------------------------------------------------------------------------
// Calls to (IList<T>)(array).Meth are actually implemented by SZArrayHelper.Meth<T>
// This workaround exists for two reasons:
//
//    - For working set reasons, we don't want insert these methods in the array 
//      hierachy in the normal way.
//    - For platform and devtime reasons, we still want to use the C# compiler to 
//      generate the method bodies.
//
// (Though it's questionable whether any devtime was saved.)
//
// ....
//----------------------------------------------------------------------------------

所以,這樣作是爲了方便高效,由於他們不但願 System.Array 的每一個實例都攜帶 IEnumera<T>IList<T> 實現的全部代碼。

此映射經過調用 GetActualImplementationForArrayGenericIListOrIReadOnlyListMethod(..),。它負責從 SZArrayHelper 鏈接相應的方法類,即 IList<T>.Count -> SZArrayHelper.Count<T>,或者若是該方法是 IEnumerator<T> 接口的一部分,則使用 SZGenericArrayenumerator

可是,這有可能致使安全漏洞,由於它打破了正常的 C# 類型系統保證,特別是關於this指針。爲了說明這個問題,下面是 Count 屬性的源代碼,請注意對 JitHelpers.UnsafeCast<T[]> 的調用。

internal int get_Count<T>()
{
    //! Warning: "this" is an array, not an SZArrayHelper. See comments above
    //! or you may introduce a security hole!
    T[] _this = JitHelpers.UnsafeCast<T[]>(this);
    return _this.Length;
}

它必須從新映射 this,以便可以調用正確的對象的 length

譯者補充:正如上面的註釋所描述,get_Count<T>SZArrayHelper中的實例方法,而this並非指SZArrayHelper,而是指數組。

以防這些註釋描述的不夠,在這個類的頂部有一個措辭很是強烈的註釋,進一步闡明瞭風險!!

通常來講,全部這些「魔術」都是隱藏的,但偶爾它會向外暴露出來。例如,若是你運行如下代碼,SZArrayHelper 將顯示在StackTraceTargetSite中:

try { 
    int[] someInts = { 1, 2, 3, 4 };
    IList<int> collection = someInts;
    // Throws NotSupportedException 'Collection is read-only'
    collection.Clear();         
} catch (NotSupportedException nsEx) {              
    Console.WriteLine("{0} - {1}", nsEx.TargetSite.DeclaringType, nsEx.TargetSite);
    Console.WriteLine(nsEx.StackTrace);
}

System.SZArrayHelper - Void Clear[T]()
   在 System.SZArrayHelper.Clear[T]()

移除邊界檢查

運行時還以更傳統的方式爲數組提供了支持,其中第一種與性能有關。數組邊界檢查提供了很好的內存安全,但它們有開銷成本,所以,如有可能,JIT 會刪除任何它所知道的冗餘檢查。

它經過計算for循環訪問的值範圍並將這些值與數組的實際長度進行比較來實現該功能。若是它肯定從未嘗試訪問數組邊界之外的項,就會刪除運行時檢查。

更多詳細信息,如下連接將帶你處處理此狀況的 JIT 源代碼:

若是你感興趣,看看這裏,我將數組檢索邊界檢查的「刪除」和「不刪除」的場景放到了一塊兒。

分配數組

運行時所提供幫助的另外一個任務是使用手寫的程序集代碼分配數組,以便儘量優化方法,請參閱:

運行時以不一樣的方式對待數組

最後,因爲數組與 CLR 緊密聯繫在一塊兒,所以在不少地方,它們都被做爲特殊狀況進行處理。例如,在 CoreCLR 源碼中搜索IsArray()會返回超過 60 條記錄,包括:

因此,公平地說,數組和CLR有一個很是特殊的關係

進一步閱讀

和往常同樣,這裏有一些更多的連接提供給你閱讀

數組源碼引用

參考文檔

  1. StructLayout特性
  2. Compiling C# Code Into Memory and Executing It with Roslyn
  3. .NET CLR 運行原理
  4. Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects
  5. How do arrays in C# partially implement IList ?
  6. .NET Arrays, IList , Generic Algorithms, and what about STL?

20191127212134.png
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:http://www.javashuo.com/article/p-ouisadzk-ee.html 做者:傑哥很忙 本文使用「CC BY 4.0」創做共享協議。歡迎轉載,請在明顯位置給出出處及連接。

相關文章
相關標籤/搜索