原文地址:https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-3-the-layout-of-a-managed-array-3/
原文做者:Sergey
譯文做者:傑哥很忙c++
託管對象本質1-佈局
託管對象本質2-對象頭佈局和鎖成本
託管對象本質3-託管數組結構
託管對象本質4-字段佈局git
數組是每一個應用程序的基本構建基塊之一。即便你不是天天直接使用數組,你也能夠將他們做爲庫包的一部分間接使用。github
C# 一直都有數組結構,數組結構也是惟一一個相似泛型並且類型安全的數據結構。如今你可能沒有那麼頻繁的直接使用他們,可是爲了提高性能,都有可能會從一些更高級的數據結構(好比List<T>
)切換回數組。c#
數組和CLR有着很是特殊的關係,可是今天咱們將從用戶的角度來探討它們。咱們將討論如下內容:
* 探索一個最奇怪的 C# 功能,稱爲數組協方差
* 討論數組的內部結構
* 探索一些性能技巧,咱們能夠這樣作,從數組中擠壓更多的性能數組
C# 語言中最奇怪的特徵之一是數組協方差:可以將T類型的數組賦值給object類型或任何其餘T類型的基類的數組的能力。緩存
string[] strings = new[] { "1", "2" }; object[] objects = strings;
這個轉換並徹底是類型安全的。若是objects變量僅用於讀取數據,那麼一切正常。可是,若是嘗試修改數組時,若是參數的類型不兼容,則可能會失敗。安全
objects[0] = 42; //運行時錯誤
關於這個特性,.NET 社區中有一個衆所周知的笑話:C# 做者在一開始很是努力地將 Java 生態系統的各個方面複製到 CLR 世界,因此他們也複製了語言設計問題。微信
可是我並不認爲這是緣由:)數據結構
在 90 年代後期,CLR 並無泛型,對嗎?在這種狀況下,語言用戶如何編寫處理任意數據類型數組的可重用代碼?例如,如何編寫將任意數組轉儲到控制檯的函數?
一種方法是定義接收object[]
的函數,並經過將數組複製到對象數組來強制每一個調用方手動轉換數組。這是可行的,但效率很低。另外一種解決方案是容許從任何引用類型的數組轉換爲object[]
,即保留 Derived[]
到 Base[]
的 IS-A
關係,其中派生類從基類繼承。在值類型的數組中,轉換不起做用,但至少能夠實現一些通用性。
第一個 CLR 版本中缺乏泛型,迫使設計人員削弱類型系統。可是這個決定(我想)是通過深思熟慮的,而不只僅是來自Java生態系統的模仿。
數組協方差在編譯時在類型系統中打開一個洞,但這並不意味着類型錯誤會使應用程序崩潰(C++中的相似"錯誤"將致使"未定義行爲")。CLR 將確保類型安全,但檢查會在運行時進行。爲此,CLR 必須存儲數組元素的類型,並在用戶嘗試更改數組實例時進行檢查。幸運的是,此檢查僅對引用類型的數組是必需的,由於struct是密封的(sealed
),所以不支持繼承。
譯者補充:struct因爲是值類型,咱們能夠查看它的IL語言,能夠看到struct前會有
sealed
關鍵字。
儘管不一樣值類型(如 int
到 byte
)之間存在隱式轉換,但 int[]
和 byte[]
之間沒有隱式或顯式轉換。數組協方差轉換是引用轉換,它不會更改轉換對象的佈局,並保留要轉換對象的引用標識。
在舊版本的 CLR 中,引用數組和值類型具備不一樣的佈局。引用類型的數組具備對每一個實例中元素的類型句柄的引用:
這在最新版本的 CLR 中已更改,如今元素類型存儲在方法表中:
有關佈局的詳細信息,請參閱 CoreClr 代碼庫中的如下代碼段:
// Get the element type for the array, this works whether the element // type is stored in the array or not inline TypeHandle GetArrayElementTypeHandle() const;
TypeHandle GetArrayElementTypeHandle() { LIMITED_METHOD_CONTRACT; return GetMethodTable()->GetApproxArrayElementTypeHandle(); }
TypeHandle GetApproxArrayElementTypeHandle() { LIMITED_METHOD_DAC_CONTRACT; _ASSERTE(IsArray()); return TypeHandle::FromTAddr(m_ElementTypeHnd); }
union { PerInstInfo_t m_pPerInstInfo; TADDR m_ElementTypeHnd; TADDR m_pMultipurposeSlot1; };
我不肯定數組佈局是何時改變的,但彷佛在速度和(託管)內存之間有一個權衡。因爲內存局部性,初始實現(當類型句柄存儲在每一個數組實例中時)訪問應該更快,但確定有不可忽略的內存開銷。當時,全部引用類型的數組都有共享方法表。但如今狀況不同了:每一個引用類型的數組都有本身的方法表,該表指向相同的 EEClass ,指針指向元素類型句柄的指針。
也許CLR團隊的人能夠解釋一下.
咱們知道 CLR 如何存儲數組的元素類型,如今咱們能夠探索 CoreClr 代碼庫,看看實現類型檢查。
首先,咱們須要找到檢查發生的位置。數組是 CLR 的一種很是特殊的類型,IDE 中沒有"轉到聲明"按鈕來"反編譯"數組並顯示源代碼。可是咱們知道,檢查發生在索引器setter中,它與一組IL指令StElem*
相對應:
* StElem.i4 用於整型數組
* StElem 用於任意值類型數組
* StElem.ref 用於引用類型數組
瞭解指令後,咱們能夠輕鬆地在代碼庫中找到實現。據我所知,實如今了jithelpers.cpp中。下面是方法JIT_Stelem_Ref_Portable
稍微簡化的版本:
/****************************************************************************/ /* assigns 'val to 'array[idx], after doing all the proper checks */ HCIMPL3(void, JIT_Stelem_Ref_Portable, PtrArray* array, unsigned idx, Object *val) { FCALL_CONTRACT; if (!array) { // ST: explicit check that the array is not null FCThrowVoid(kNullReferenceException); } if (idx >= array->GetNumComponents()) { // ST: bounds check FCThrowVoid(kIndexOutOfRangeException); } if (val) { MethodTable *valMT = val->GetMethodTable(); // ST: getting type of an array element TypeHandle arrayElemTH = array->GetArrayElementTypeHandle(); // ST: g_pObjectClass is a pointer to EEClass instance of the System.Object // ST: if the element is object than the operation is successful. if (arrayElemTH != TypeHandle(valMT) && arrayElemTH != TypeHandle(g_pObjectClass)) { // ST: need to check that the value is compatible with the element type TypeHandle::CastResult result = ObjIsInstanceOfNoGC(val, arrayElemTH); if (result != TypeHandle::CanCast) { // ST: ArrayStoreCheck throws ArrayTypeMismatchException if the types are incompatible if (HCCALL2(ArrayStoreCheck, (Object**)&val, (PtrArray**)&array) != NULL) { return; } } } HCCALL2(JIT_WriteBarrier, (Object **)&array->m_Array[idx], val); } else { // no need to go through write-barrier for NULL ClearObjectReference(&array->m_Array[idx]); } }
如今咱們知道,CLR 確實在底層確保引用類型數組的類型安全。對數組實例的每一個"寫入"都有一個附加檢查,若是數組在熱路徑上中使用,則該檢查不可忽略。但在得出錯誤的結論以前,讓咱們先看看這個檢查的性能消耗程度。
譯者補充:熱路徑指的是那些會被頻繁調用的代碼塊。
爲了不檢查,咱們能夠更改 CLR,或者使用一個衆所周知的技巧:將對象包裝到結構中
public struct ObjectWrapper { public readonly object Instance; public ObjectWrapper(object instance) { Instance = instance; } }
比較 object[] 和 ObjectWrapper[]的時間
private const int ArraySize = 100_000; private object[] _objects = new object[ArraySize]; private ObjectWrapper[] _wrappers = new ObjectWrapper[ArraySize]; private object _objectInstance = new object(); private ObjectWrapper _wrapperInstanace = new ObjectWrapper(new object()); [Benchmark] public void WithCheck() { for (int i = 0; i < _objects.Length; i++) { _objects[i] = _objectInstance; } } [Benchmark] public void WithoutCheck() { for (int i = 0; i < _objects.Length; i++) { _wrappers[i] = _wrapperInstanace; } }
結果以下:
Method | 平均值 | 錯誤 | 標準差 |
---|---|---|---|
WithCheck | 807.7 us | 15.871 us | 27.797 us |
WithoutCheck | 442.7 us | 9.371 us | 8.765 us |
不要被"幾乎 2 倍"的性能差別所迷惑。即便在最壞的狀況下,分配 100K 元素也不到一毫秒。性能表現很是好。但在現實世界中,這種差別是顯而易見的。
許多性能關鍵的 .NET 應用程序使用對象池。池容許重用託管實例,而無需每次都建立新實例。此方法下降了內存壓力,並可能對應用程序性能產生很是合理的影響。
能夠基於併發數據結構(如ConcurrentQueue)或基於簡單數組實現對象池。下面是 Roslyn 代碼庫中對象池實現的代碼段:
internal class ObjectPool<T> where T : class { [DebuggerDisplay("{Value,nq}")] private struct Element { internal T Value; } // Storage for the pool objects. The first item is stored in a dedicated field because we // expect to be able to satisfy most requests from it. private T _firstItem; private readonly Element[] _items; // other members ommitted for brievity }
該實現管理一個緩存項數組,但池化並非直接使用 T[]
,而是將 T 包裝到結構元素中,以免在運行時進行檢查。
前段時間,我在應用程序中修復了一個對象池,使得解析階段的性能提高了的 30%。這不是因爲我在這裏描述的技巧,是與池的併發訪問相關。但關鍵是,對象池可能位於應用程序的熱路徑上,甚至像上面提到的小性能改進也可能對總體性能產生明顯影響。
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:http://www.javashuo.com/article/p-xvddyhvn-bd.html 做者:傑哥很忙 本文使用「CC BY 4.0」創做共享協議。歡迎轉載,請在明顯位置給出出處及連接。