前言
好久没有动用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="@x9bxd6xxffU0000007f") 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,文章首发,欢迎关注