获取C#中的分配总数

Get total number of allocations in C#

有没有办法获取分配总数(注意 - 分配数,而不是分配的字节数)?它可以是针对当前线程的,也可以是全局的,以更容易的为准。

我想检查一个特定函数分配了多少个对象,虽然我知道调试 -> 性能探查器 (Alt+F2),但我希望能够从我的程序内部以编程方式执行此操作。

// pseudocode
int GetTotalAllocations() {
    ...;
}    
class Foo {
    string bar;
    string baz;
}
public static void Main() {
    int allocationsBefore = GetTotalAllocations();
    PauseGarbageCollector(); // do I need this? I don't want the GC to run during the function and skew the number of allocations
    // Some code that makes allocations.
    var foo = new Foo() { bar = "bar", baz = "baz" };
    ResumeGarbageCollector();
    int allocationsAfter = GetTotalAllocations();
    Console.WriteLine(allocationsAfter - allocationsBefore); // Should print 3 allocations - one for Foo, and 2 for its fields.
}

此外,我是否需要暂停垃圾收集才能获得准确的数据,我可以这样做吗?

我需要使用 CLR Profiling API 来实现吗?

您需要使用一些 kernel32 功能,但这是可能的!!:) 我没有写完整的代码,但我希望您能感觉到应该如何完成。

首先,您需要所有具有功能的进程:Process.GetProcesses link 然后你需要从它创建一个快照 CreateToolhelp32Snapshot 因为这个快照不需要 "pause of the GC",并且在你需要创建循环以枚举所有内存块之后。循环函数用 Heap32ListFirstHeap32First 初始化,之后你可以调用 Heap32Next 直到它成功。

您可以调用 kerner32 函数,当它在您的代码中这样声明时:

[DllImport("kernel32", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
static extern IntPtr CreateToolhelp32Snapshot([In]UInt32 dwFlags, [In]UInt32 th32ProcessID);

这是 C++ 示例,但您也可以在 CSharp 函数声明之后执行相同的操作:Traversing the Heap List

我知道这并不容易,但没有简单的方法。顺便说一下,如果您在循环内调用 Toolhelp32ReadProcessMemory,您可以检索很多有用的其他信息。


我发现 pinvoke.net 也许对你有帮助 pinvoke.net

https://www.pinvoke.net/default.aspx/kernel32.createtoolhelp32snapshot https://www.pinvoke.net/default.aspx/kernel32.Heap32ListFirst

首先,您可以通过调用 System.GC.TryStartNoGCRegion and unpause it with System.GC.EndNoGCRegion.

来暂停 GC

只知道分配了多少 字节 System.GC.GetAllocatedBytesForCurrentThread 即 returns 为当前线程分配的总字节数。代码前后分别调用来衡量,区别就是分配大小。

计算分配数量有点棘手。可能有很多方法可以做到这一点,但在今天,这些方法在某种程度上都不是最优的。我能想到一个主意:

修改默认GC

从 .NET Core 2.1 开始,可以使用自定义 GC,即所谓的 local GC。据说开发经验、文档和实用性不是最好的,但根据你的问题的细节,它可能对你有帮助。

每次分配对象时,运行时调用 Object* IGCHeap::Alloc(gc_alloc_context * acontext, size_t size, uint32_t flags)IGCHeap 定义为 here with the default GC implementation here(GCHeap::Alloc 在第 37292 行中实现)。

在这里要交谈的人是 Konrad Kokosa with two presentations on that topic: #1, #2, slides

我们可以按原样采用默认的 GC 实现并修改 Alloc 方法以在每次调用时增加一个计数器。

在托管代码中公开计数器

接下来要使用新的计数器,我们需要一种从托管代码中使用它的方法。为此,我们需要修改运行时。在这里,我将描述如何通过扩展 GC 接口(由 System.GC 公开)来做到这一点。

注意:我没有这方面的实际经验,走这条路可能会遇到一些问题。我只是想准确表达我的想法。

通过查看 ulong GC.GetGenerationSize(int),我们能够了解如何添加导致内部 CLR 调用的方法。

打开\runtime\src\coreclr\src\System.Private.CoreLib\src\System\GC.cs#112并声明一个新方法:

[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern ulong GetAllocationCount();

接下来,我们需要在本机 GCInterface 上定义该方法。为此,转到 runtime\src\coreclr\src\vm\comutilnative.h#112 并添加:

static FCDECL0(UINT64, GetAllocationCount);

到link这两个方法,需要在runtime\src\coreclr\src\vm\ecalllist.h#745中列出:

FCFuncElement("GetAllocationCount", GCInterface::GetAllocationCount)

最后,实际实现 runtime\src\coreclr\src\vm\comutilnative.cpp#938 处的方法:

FCIMPL0(UINT64, GCInterface::GetAllocationCount)
{
    FCALL_CONTRACT;

    return (UINT64)(GCHeapUtilities::GetGCHeap()->GetAllocationCount());
}
FCIMPLEND

这会得到一个指向我们的分配计数器所在的 GCHeap 的指针。上面暴露this的方法GetAllocationCount还不存在,所以让我们创建它:

runtime\src\coreclr\src\gc\gcimpl.h#313

size_t GetAllocationCount();

runtime\src\coreclr\src\gc\gcinterface.h#680

virtual size_t GetAllocationCount() = 0;

runtime\src\coreclr\src\gc\gcee.cpp#239

size_t GCHeap::GetAllocationCount()
{
    return m_ourAllocationCounter;
}

为了使我们的新方法 System.GC.GetAllocationCount() 可用于托管代码,我们需要针对自定义 BCL 进行编译。也许自定义 NuGet 包也可以在这里工作(如上所示,它定义 System.GC.GetAllocationCount() 为内部调用)。

关闭

诚然,如果以前没有做过,这将是相当多的工作,自定义 GC + CLR 在这里可能有点矫枉过正,但我​​认为我应该把它扔在那里作为一种可能性。

此外,我还没有测试过这个。你应该把它当作一个概念。

您可以记录每一次分配。但是你在你的过程中这样做的逻辑是有缺陷的。 .NET Core 支持进程内 ETW 数据收集,这使得记录所有分配事件成为可能。 参见

Starting with .NET Core 2.2, CoreCLR events can now be consumed using the System.Diagnostics.Tracing.EventListener class. These events describe the behavior of such runtime services as GC, JIT, ThreadPool, and interop. These are the same events that are exposed as part of the CoreCLR ETW provider. This allows for applications to consume these events or use a transport mechanism to send them to a telemetry aggregation service. You can see how to subscribe to events in the following code sample:

internal sealed class SimpleEventListener : EventListener
{
    // Called whenever an EventSource is created.
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        // Watch for the .NET runtime EventSource and enable all of its events.
        if (eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime"))
        {
            EnableEvents(eventSource, EventLevel.Verbose, (EventKeywords)(-1));
        }
    }

    // Called whenever an event is written.
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        // Write the contents of the event to the console.
        Console.WriteLine($"ThreadID = {eventData.OSThreadId} ID = {eventData.EventId} Name = {eventData.EventName}");
        for (int i = 0; i < eventData.Payload.Count; i++)
        {
            string payloadString = eventData.Payload[i]?.ToString() ?? string.Empty;
            Console.WriteLine($"\tName = \"{eventData.PayloadNames[i]}\" Value = \"{payloadString}\"");
        }
        Console.WriteLine("\n");
    }
}

当您启用 GC evets (0x1) 而不是 -1 时,应该给出所有 GC 暂停时间和您需要在进程中诊断自己的 GC 事件。

.NET Core 和 .NET Framework 中内置了分配抽样机制,可以对最多 5 个 alloc events/s GC_Alloc_Low 或 100 个 alloc events/s 对象分配指标进行抽样] GC_Alloc_High 分配的对象。似乎没有办法获取所有分配事件,但如果您阅读 .NET Core 代码

BOOL ETW::TypeSystemLog::IsHeapAllocEventEnabled()
{
    LIMITED_METHOD_CONTRACT;

    return
        // Only fire the event if it was enabled at startup (and thus the slow-JIT new
        // helper is used in all cases)
        s_fHeapAllocEventEnabledOnStartup &&

        // AND a keyword is still enabled.  (Thus people can turn off the event
        // whenever they want; but they cannot turn it on unless it was also on at startup.)
        (s_fHeapAllocHighEventEnabledNow || s_fHeapAllocLowEventEnabledNow);
}

你发现当

时你可以通过ETW获取所有分配事件
  1. 进程启动时必须启用 ETW 分配分析(稍后启用将不起作用)
  2. GC_Alloc_High AND GC_Allow_Low 关键字已启用

如果存在记录分配分析数据的 ETW 会话,您可以在 .NET Core 2.1+ 进程中记录所有分配。

样本:

C>perfview collect  c:\temp\perfViewOnly.etl -Merge:true -Wpr -OnlyProviders:"Microsoft-Windows-DotNETRuntime":0x03280095::@StacksEnabled=true
C>AllocTracker.exe
    Microsoft-Windows-DotNETRuntime
    System.Threading.Tasks.TplEventSource
    System.Runtime
    Hello World!
    Did allocate 24 bytes
    Did allocate 24 bytes
    Did allocate 24 bytes
    Did allocate 76 bytes
    Did allocate 76 bytes
    Did allocate 32 bytes
    Did allocate 64 bytes
    Did allocate 24 bytes
    ... endless loop!

    using System;
    using System.Diagnostics.Tracing;

    namespace AllocTracker
    {
        enum ClrRuntimeEventKeywords
        {
            GC = 0x1,
            GCHandle = 0x2,
            Fusion = 0x4,
            Loader = 0x8,
            Jit = 0x10,
            Contention = 0x4000,
            Exceptions                   = 0x8000,
            Clr_Type                    = 0x80000,
            GC_AllocHigh =               0x200000,
            GC_HeapAndTypeNames       = 0x1000000,
            GC_AllocLow        =        0x2000000,
        }

        class SimpleEventListener : EventListener
        {
            public ulong countTotalEvents = 0;
            public static int keyword;

            EventSource eventSourceDotNet;

            public SimpleEventListener() { }

            // Called whenever an EventSource is created.
            protected override void OnEventSourceCreated(EventSource eventSource)
            {
                Console.WriteLine(eventSource.Name);
                if (eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime"))
                {
                    EnableEvents(eventSource, EventLevel.Informational, (EventKeywords) (ClrRuntimeEventKeywords.GC_AllocHigh | ClrRuntimeEventKeywords.GC_AllocLow) );
                    eventSourceDotNet = eventSource;
                }
            }
            // Called whenever an event is written.
            protected override void OnEventWritten(EventWrittenEventArgs eventData)
            {
                if( eventData.EventName == "GCSampledObjectAllocationHigh")
                {
                    Console.WriteLine($"Did allocate {eventData.Payload[3]} bytes");
                }
                    //eventData.EventName
                    //"BulkType"
                    //eventData.PayloadNames
                    //Count = 2
                    //    [0]: "Count"
                    //    [1]: "ClrInstanceID"
                    //eventData.Payload
                    //Count = 2
                    //    [0]: 1
                    //    [1]: 11

                    //eventData.PayloadNames
                    //Count = 5
                    //    [0]: "Address"
                    //    [1]: "TypeID"
                    //    [2]: "ObjectCountForTypeSample"
                    //    [3]: "TotalSizeForTypeSample"
                    //    [4]: "ClrInstanceID"
                    //eventData.EventName
                    //"GCSampledObjectAllocationHigh"
            }
        }

        class Program
        {
            static void Main(string[] args)
            {
                SimpleEventListener.keyword = (int)ClrRuntimeEventKeywords.GC;
                var listener = new SimpleEventListener();

                Console.WriteLine("Hello World!");

                Allocate10();
                Allocate5K();
                GC.Collect();
                Console.ReadLine();
            }
            static void Allocate10()
            {
                for (int i = 0; i < 10; i++)
                {
                    int[] x = new int[100];
                }
            }

            static void Allocate5K()
            {
                for (int i = 0; i < 5000; i++)
                {
                    int[] x = new int[100];
                }
            }
        }

    }

现在您可以在记录的ETL 文件中找到所有分配事件。一种分配 10 的方法和另一种分配 5000 数组分配的方法。

之所以我告诉你你的逻辑是有缺陷的,是因为即使是像将分配事件打印到控制台这样的简单操作也会分配对象。你知道这会在哪里结束吗? 如果你想实现完整的代码路径必须是无分配的,我猜这是不可能的,因为至少 ETW 事件监听器需要分配你的事件数据。 您已经达到了目标,但使您的应用程序崩溃了。因此,我会依赖 ETW 并从外部记录数据,或者使用出于同样原因需要不受管理的探查器来记录数据。

使用 ETW,您可以获得所有分配堆栈和类型信息,您不仅需要报告这些信息,还可以找到有问题的代码片段。关于方法内联还有更多内容,但我想这对于 SO post 已经足够了。