1. 程式人生 > >託管物件本質-第一部分-佈局

託管物件本質-第一部分-佈局

目錄

  • 託管物件本質-第一部分-佈局
    • 目錄
    • IsMarked 標記
    • 相關文獻


託管物件本質-第一部分-佈局

原文地址:https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-1-layout/
原文作者:Sergey
譯文作者:傑哥很忙

目錄

託管物件本質1-佈局
託管物件本質2-物件頭佈局和鎖成本
託管物件本質3-託管陣列結構
託管物件本質4-欄位佈局

託管物件的佈局非常簡單:託管物件包含例項資料、指向元資料的指標(也稱為方法表指標)和內部資訊包(也稱為物件頭)。

譯者補充: 方法表指標在某些文章也被稱之為型別控制代碼,英文是TypeHandle。

當我第一次看到物件的佈局時,我產生了一些疑問:

  1. 為什麼物件的佈局如此怪異?
  2. 為什麼託管引用指向物件的中間,而物件頭的偏移量為負?
  3. 物件頭中儲存了哪些資訊?

譯者補充:作者對於佈局怪異實際指的就是物件頭的偏移量為負數。由於託管物件以引用地址的偏移量記為0,物件頭大小為4或8位元組(取決於是32位還是64位,實際64位物件頭也僅使用4位元組,前面4個位元組填充0)。因此由於物件頭在物件指標之前,因此它的偏移量位-4或-8。

當我開始思考佈局並做了一個快速研究時,我只有幾個選擇:

  1. JVM 從一開始就對託管物件使用了類似的佈局。
    今天聽起來有點瘋狂,但請記住,由於Java早就有了一個特性(又名陣列協方差),C#在借鑑Java語言時也引用了這一有史以來最糟糕的特性。與這個決定相比,重用一些關於物件結構的想法聽起來並不合理。

    譯者補充:陣列協方差可能存在無法保證型別安全,從而產生一個執行時異常。詳情可以看與C#陣列的協方差和逆差

  2. 物件頭的大小可以增大,而在 CLR 中沒有橫切更改。
    物件頭包含 CLR 使用的一些輔助資訊,CLR 可能需要比指標大小欄位更多的資訊。事實上,行動電話中使用的 .Net Compact Framework 對於大小物件具有不同的頭(有關詳細資訊,請參閱 WP7:CLR 託管物件開銷)。桌面 CLR 從未使用過此功能,但這並不意味著將來不可能實現此功能。

    譯者補充:這裡說的CLR沒有根據切面大小改變物件頭,指的是桌面CLR,因為移動裝置的CLR會根據物件大小改變物件頭的佈局。

  3. 快取行和其他效能相關特徵。

Chris Brumme – CLR 架構師之一,在他的發表的Value Types的評論中提到,快取友好性正是託管物件佈局的原因。從理論上講,由於快取行大小(64 位元組),訪問彼此較近的欄位的效率可能更高。這意味著根據欄位在物件中的位置不同,訪問間接引用欄位會有不同的效能差異。我花了一些時間試圖證明對於現代處理器該理論依據仍然是成立的,但無法獲得任何能夠顯示存在的差異基準測試資料。

花了一些時間試圖驗證我的理論後,我聯絡了Vance Morrison問這個問題,並得到了以下的答案:目前的設計沒有特別考慮。

因此,對於"為什麼託管物件的佈局如此怪異?"的一個簡單回答是由於歷史原因造成的。老實說,我可以看到在負索引移動物件頭的邏輯,以強調此資料塊是 CLR 的實現細節,它的大小可以隨時間而變化,並且不應由使用者檢查。

譯者補充:原作者說的是由於歷史原因造成的也沒有毛病,因為非託管程式碼就包含了物件頭和物件引用地址,因此託管程式碼延續了這一風格。

現在是時候審視佈局的更多細節了。再次之前,我們思考一下,CLR可以與託管物件例項關聯哪些額外資訊?以下是一些想法:

  • GC可以用來標記可從應用程式根訪問物件的特殊標誌。
  • 一種特殊的標誌用於通知GC某個物件已固定,在垃圾收集期間不應移動。
  • 託管物件的雜湊程式碼(當未重寫GetHashCode方法時)。
  • 鎖語句使用的關鍵節和其他資訊:獲取鎖的執行緒等。

除了例項狀態之外,CLR還儲存了許多與型別相關的資訊,如方法表、介面對映、例項大小等等,但這與我們當前的討論無關。

IsMarked 標記

託管物件頭可用於多種不同的用途。你可能認為垃圾收集器(GC)使用物件頭中的一個位來標記該物件是由根引用的,並且應該保持活動狀態。這是一種常見的誤解,只有少量的名著提及。

比如Jeffrey Richter寫的《CLR via C#》, 《Pro .NET Performance》作者是Sasha Goldstein,當然還有一些其他人.

CLR 作者決定不使用物件頭,而是使用一個巧妙的技巧:方法表指標的最低位用於儲存在垃圾回收期間儲存物件可訪問且不應被回收的標誌。

下面是來自Coreclr的一個mark標記的實現,在檔案gc.cpp的8974行:

#define marked(i) header(i) -> IsMmarked();
#define set_marked(i) header(i)->SetMarked()
#define clear_marked(i) header(i)->ClearMarked()
 
// class CObjectHeader
BOOL IsMarked() const
{
    return !!(((size_t)RawGetMethodTable()) & GC_MARKED);
}
void ClearMarked()
{
    RawSetMethodTable(GetMethodTable());
}
void SetMarked()
{
    RawSetMethodTable((MethodTable*)(((size_t)RawGetMethodTable()) | GC_MARKED));
}
MethodTable* GetMethodTable() const
{
    return((MethodTable*)(((size_t)RawGetMethodTable()) & (~(GC_MARKED))));
}

由於gc.cpp檔案太大導致GitHub不分析它。 這意味著我不能將超連結新增到特定程式碼行。

CLR 堆中的託管指標以4個或8個位元組地址長度進行對齊,取決於32位還是64位平臺。這意味著每個指標的 2 或 3 位始終為 0,可用於其他目的。JVM 也使用同樣的技巧,稱為"壓縮 Oops",該功能允許 JVM 具有 32 GB 堆大小,並且仍使用 4 個位元組作為託管指標。

譯者補充:當我們在物件上標註StructLayout以控制物件的分佈甚至偏移值。若物件沒有填滿4位元組或8位元組時,CLR會進行自動填充。
“這意味著每個指標的 2 或 3 位始終為 0,可用於其他目的。”對於這句話的解釋,個人理解如下:由於64位指標最多支援2^64^記憶體,即16TiB的記憶體大小,而對於windows系統則有軟體上的記憶體大小限制,windows7旗艦版支援192GB的記憶體,而windows server 2008 R2支援2TiB記憶體大小,Windows Server 2012提高到4TiB的最大記憶體限制。因此可以如作者所說,windows 64位作業系統預留了2到3位指標用於其他目的,因此最大記憶體支援4TiB。

從技術上講,即使在 32 位平臺上,也有 2 位可用於標誌。基於 object.h 檔案的註釋,我們可以認為確實如此,並且方法表指標的第二個最低位用於固定(以標記在垃圾回收的壓縮階段不應移動物件)。不幸的是,並不能判斷該說法是否正確,因為來自 gc.cpp(行 3850-3859)的 SetPinned/IsPinned 方法基於物件頭中的保留位實現,並且我無法在 coreclr 程式碼版本庫中找到實際設定方法表指標位的任何程式碼。

下次我們會討論所得實現以及鎖的效能消耗大小。

相關文獻

  1. 陣列協方差
  2. CPU快取記憶體行對齊(cache line)
  3. 型別例項的建立位置、託管物件在託管堆上的結構
  4. .net託管環境下struct例項欄位的記憶體佈局(Layout)和大小(Size)
  5. 託管堆上物件的大小(Size)和Layout
  6. .NET物件的記憶體佈局
  7. .NET Framework Internals: How the CLR Creates Runtime Objects
  8. What limits Windows 7 x64 machines to <=192GB RAM?


微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:https://www.cnblogs.com/Jack-Blog/p/12230616.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及連結。