web-dev-qa-db-ja.com

C#で割り当ての総数を取得する

割り当ての総数を取得する方法はありますか(注-割り当てられたバイト数ではなく、割り当て数)。現在のスレッドの場合も、グローバルな場合も、どちらでも簡単です。

特定の関数が割り当てるオブジェクトの数を確認したいのですが、Debug-> Performance Profiler(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プロファイリングAPIを使用する必要がありますか?

4
sashoalm

すべての割り当てを記録できます。しかし、プロセス内でこれを行うロジックには欠陥があります。 .NET Coreはプロセス内のETWデータ収集をサポートしており、すべての割り当てイベントを記録することもできます。見る

.NET Core 2.2以降では、System.Diagnostics.Tracing.EventListenerクラスを使用してCoreCLRイベントを使用できるようになりました。これらのイベントは、GC、JIT、ThreadPool、interopなどのランタイムサービスの動作を記述します。これらは、CoreCLR ETWプロバイダーの一部として公開されるイベントと同じです。これにより、アプリケーションはこれらのイベントを使用したり、トランスポートメカニズムを使用してそれらをテレメトリ集約サービスに送信したりできます。イベントをサブスクライブする方法は、次のコードサンプルで確認できます。

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");
    }
}

-1ではなくGC evets(0x1)を有効にすると、すべてのGC一時停止時間と、インプロセスで診断する必要があるGCイベントが有効になります。

.NET Coreと.NET Frameworkに組み込まれた割り当てサンプリングメカニズムが古くから存在しており、最大5つの割り当てイベント/秒GC_Alloc_Lowまたは100割り当てイベント/秒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の配列割り当てを持つ別の方法。

PerfView Allocation Recording

ロジックに欠陥があると私が言った理由は、割り当てイベントをコンソールに出力するような単純な操作でもオブジェクトを割り当てるためです。これがどこに行くのか分かりますか?完全なコードパスを割り当て不要にする必要がある場合、少なくともETWイベントリスナーがイベントデータを割り当てる必要があるため、これは不可能です。目標を達成しましたが、アプリケーションがクラッシュしました。したがって、私はETWに依存し、外部から、または同じ理由で管理されない必要があるプロファイラーを使用してデータを記録します。

ETWを使用すると、レポートだけでなく、問題のあるコードスニペットを見つけるために必要なすべての割り当てスタックとタイプ情報を取得できます。メソッドのインライン化についてはそれだけではありませんが、SOの投稿ではおそらくそれで十分でしょう。

1
Alois Kraus