前言
好久沒有動用LLDB了,本篇透過它來看下FOH也就是.NET8裡面最佳化字串,為了提高其效能增加的FOH堆分配過程。關於FOH可以參考:.NET8極致效能最佳化Non-GC Heap
詳細
來看一個簡單的例子:
public static string GetPrefix() => "https://";
static void Main(string[] args)
{
GetPrefix();
GC.Collect();
Console.ReadLine();
}
函式GetPrefix裡面的字串“https://”就是被分配到FOH堆裡面的,如何驗證呢?
首先透過LLDB把CLR執行到託管Main入口
(lldb) b RunMainInternal
Breakpoint 7: where = libcoreclr.so`RunMainInternal(Param*) at assembly.cpp:1257, address = 0x00007ffff6d43930
(lldb) r
Process 2697 launched: '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64)
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 6.1 7.1
frame #0: 0x00007ffff6d43930 libcoreclr.so`RunMainInternal(pParam=0x00007ffff7faaab6) at assembly.cpp:1257
1254 } param;
1255
1256 static void RunMainInternal(Param* pParam)
-> 1257 {
1258 MethodDescCallSite threadStart(pParam->pFD);
1259
1260 PTRARRAYREF StrArgArray = NULL;
(lldb)
然後把其執行到JIT前置入口
(lldb) b PreStubWorker
Breakpoint 8: where = libcoreclr.so`::PreStubWorker(TransitionBlock *, MethodDesc *) at prestub.cpp:1865, address = 0x00007ffff6ee6c10
(lldb) c
Process 2697 resuming
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 8.1
frame #0: 0x00007ffff6ee6c10 libcoreclr.so`::PreStubWorker(pTransitionBlock=0x00000000ffffcb38, pMD=0x0000000155608c70) at prestub.cpp:1865
1862 // returns a pointer to the new code for the prestub's convenience.
1863 //=============================================================================
1864 extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD)
-> 1865 {
1866 PCODE pbRetVal = NULL;
1867
1868 BEGIN_PRESERVE_LAST_ERROR;
此時可以看下當前JIT編譯的函式是誰,這裡需要先n命令單步一下
(lldb) n
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = step over
frame #0: 0x00007ffff6ee6c36 libcoreclr.so`::PreStubWorker(pTransitionBlock=0x00007fffffffc648, pMD=0x00007fff78f56b70) at prestub.cpp:1866:11
1863 //=============================================================================
1864 extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD)
1865 {
-> 1866 PCODE pbRetVal = NULL;
1867
1868 BEGIN_PRESERVE_LAST_ERROR;
然後透過微軟提供的sos.dll Dump下當前的函式描述結構體MethodDesc,pMD是傳過來的函式引數,也即是MethodDesc的變數
(lldb) sos dumpmd pMD
Method Name: ConsoleApp1.Test+Program.Main(System.String[])
Class: 00007fff78f97530
MethodTable: 00007fff78f56c08
mdToken: 0000000006000008
Module: 00007fff78f542d0
IsJitted: no
Current CodeAddr: ffffffffffffffff
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 00007ffff7faa2aa
CodeAddr: 0000000000000000 (MinOptJitted)
NativeCodeVersion: 0000000000000000
可以清晰的看到Method Name就是ConsoleApp1.Test+Program.Main,OK這一步確定了,我們下面繼續尋找字串分配到FOH,首先刪掉前面所有的斷點
(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (3 breakpoints)
在TryAllocateObject 函式上下斷,它是分配託管記憶體的函式
(lldb) b TryAllocateObject
Breakpoint 9: 2 locations.
執行到此處
(lldb) c
Process 2697 resuming
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 9.1
frame #0: 0x00007ffff70300c0 libcoreclr.so`FrozenObjectHeapManager::TryAllocateObject(this=0x00007fffffff80b8, type=0x00000008017fa948, objectSize=140737488322759, publish=false) at frozenobjectheap.cpp:22
19 // May return nullptr if object is too large (larger than FOH_COMMIT_SIZE)
20 // in such cases caller is responsible to find a more appropriate heap to allocate it
21 Object* FrozenObjectHeapManager::TryAllocateObject(PTR_MethodTable type, size_t objectSize, bool publish)
-> 22 {
23 CONTRACTL
24 {
25 THROWS;
這個函式就是字串分配到FOH堆的地方,透過它的函式所在的類名即可看出FrozenObjectHeapManager,但是我們依然還是需要驗證下。繼續n單步這個函式的返回的地方,也就是如下程式碼:
202
-> 203 return object;
此時的這個object變數就是示例裡面字串的“https://”的物件地址,看下它的地址值
(lldb) p/x object
(Object *) $14 = 0x00007fffe6bff8c0
記住這個值:0x00007fffe6bff8c0,後面會把它和GC堆的範圍進行一個比較。如果它不在GC堆範圍,說明.NET8的字串確實分配在了FOH堆裡面。
我們繼續單步向下,執行到這個物件被賦值字串的地方
STRINGREF AllocateStringObject(EEStringData *pStringData, bool preferFrozenObjHeap, bool* pIsFrozen)
{
//此處省略
memcpyNoGCRefs(strDest, pStringData->GetStringBuffer(), cCount*sizeof(WCHAR));
//此處省略
}
然後看下它的記憶體:
(lldb) memory re 0x00007fffe6bff8c0
0x7fffe6bff8c0: 68 00 74 00 74 00 70 00 73 00 3a 00 2f 00 2f 00 h.t.t.p.s.:././.
0x7fffe6bff8dc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
它確實是https字串的物件地址。沒有問題。
避免干擾,此時我們再次刪除所有斷點
(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (1 breakpoint)
然後在函式is_in_find_object_range處下斷,它是在GC回收的時候,判斷當前的物件地址是否在GC堆裡面,如果是則進行物件標記,如果不是直接返回。可以透過這個獲取GC堆的範圍,執行到此處
(lldb) b is_in_find_object_range
(lldb) c
Process 2697 resuming
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 13.8
frame #0: 0x00007ffff72d881d libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) [inlined] WKS::gc_heap::is_in_find_object_range(o=0x0000000000000000) at gc.cpp:7906:11
7903 inline
7904 bool gc_heap::is_in_find_object_range (uint8_t* o)
7905 {
-> 7906 if (o == nullptr)
7907 {
7908 return false;
7909 }
單步n
(lldb) n
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = step over
frame #0: 0x00007ffff72d8831 libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) [inlined] WKS::gc_heap::is_in_find_object_range(o="@\x9b\xd6x\xff\U0000007f") at gc.cpp:7911:14
7908 return false;
7909 }
7910 #if defined(USE_REGIONS) && defined(FEATURE_CONSERVATIVE_GC)
-> 7911 return ((o >= g_gc_lowest_address) && (o < bookkeeping_covered_committed));
7912 #else //USE_REGIONS && FEATURE_CONSERVATIVE_GC
7913 if ((o >= g_gc_lowest_address) && (o < g_gc_highest_address))
7914 {
注意,此時我們看到了GC堆的一個範圍,也就是變數g_gc_lowest_address和變數g_gc_highest_address,看下它們的地址範圍
(lldb) p/x g_gc_lowest_address
(uint8_t *) $17 = 0x00007fbf68000000 ""
(lldb) p/x g_gc_highest_address
(uint8_t *) $18 = 0x00007fff68000000 "0"
上面很明顯了,GC堆的範圍起始地址:0x00007fbf68000000 ,結束地址:0x00007fff68000000 。而字串“https://”的物件地址是0x00007fffe6bff8c0,很明顯它不在GC堆的範圍內。
以上透過分配一個字串到FOH堆,後呼叫一個GC.Collect()檢視GC堆的範圍,對FOH物件地址和GC堆範圍進行一個判斷,為一個非常簡單的FOH字串分配驗證。
歡迎加入C#12/.NET8最新技術交流群
結尾
作者:jianghupt
原文:.NET8頂級除錯lldb觀察FOH堆字串分配
公眾號:jianghupt,文章首發,歡迎關注