• .NET8顶级调试lldb观察FOH堆字符串分配


    前言

    好久没有动用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,文章首发,欢迎关注

  • 相关阅读:
    记一次线上偶发慢查排查过程_es资源初始化耗时
    html中字体加粗
    JDBC入门到精通-10w总结
    IP地址、子网掩码、网络地址之间相关的计算
    【GEE笔记6】数据连接Join
    C++零基础教程(引用)
    二级缓存j2cache和SpringBoot整合
    卷积神经网络 图像处理,卷积神经网络识别图片
    世界环境日 | 周大福用心服务推动减碳环保
    路由和node环境搭建
  • 原文地址:https://www.cnblogs.com/tangyanzhi1111/p/17879198.html