Получить общее количество выделений в C# - PullRequest
4 голосов
/ 07 апреля 2020

Есть ли способ получить общее количество выделений (примечание - количество выделений, а не выделенных байтов)? Это может быть либо для текущего потока, либо глобально, в зависимости от того, что проще.

Я хочу проверить, сколько объектов выделяет конкретная функция, и пока я знаю о 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.
}

Кроме того, мне нужно приостановить сборку мусора, чтобы получить точные данные, и могу ли я это сделать?

Нужно ли использовать API профилирования CLR для достижения этой цели?

Ответы [ 3 ]

2 голосов
/ 16 апреля 2020

Сначала вы можете приостановить G C, позвонив по номеру System.GC.TryStartNoGCRegion и отменить его с System.GC.EndNoGCRegion.

Только для того, чтобы знать, сколько байтов выделено, есть System.GC.GetAllocatedBytesForCurrentThread, который возвращает общее количество байтов, выделенных для текущего потока. Вызывайте его до и после кода для измерения, и разница заключается в размере выделения.

Подсчет количества выделений немного сложнее. Есть, возможно, немало способов сделать это, которые сегодня все-таки неоптимальны. Я могу придумать одну идею:

Изменение стандартного G C

Начиная с. NET Core 2.1 есть возможность использовать пользовательский G C, так называемый местный G C. Говорят, что опыт разработки, документация и полезность не самые лучшие, но в зависимости от деталей вашей проблемы это может быть полезно для вас.

Каждый раз, когда объект выделяется, среда выполнения вызывает Object* IGCHeap::Alloc(gc_alloc_context * acontext, size_t size, uint32_t flags). IGCHeap определено здесь со стандартной реализацией G C здесь (GCHeap :: Allo c реализовано в строке 37292).

Парень в поговорить здесь будет Конрад Кокоса с двумя презентациями на эту тему c: # 1 , # 2 , слайды .

Мы можем принять реализацию G C по умолчанию как есть и изменить Alloc -метод для увеличения счетчика при каждом вызове.

Предоставление счетчика в управляемом коде

Далее, чтобы использовать новый счетчик, нам нужен способ использовать его из управляемого кода. Для этого нам нужно изменить время выполнения. Здесь я опишу, как это сделать, расширив интерфейс G C (представлен System.GC).

Примечание: у меня нет практического опыта в этом, и, вероятно, есть некоторые проблемы, возникающие при прохождении этого маршрута. Я просто хочу быть точным с моей идеей.

Взглянув на ulong GC.GetGenerationSize(int), мы можем выяснить, как добавить метод, который приводит к внутреннему вызову CLR.

Откройте \ runtime \ src \ coreclr \ src \ System.Private.CoreLib \ src \ System \ G C .cs # 112 и объявите новый метод:

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

Далее нам нужно определить этот метод на собственном GCInterface. Для этого нужно получить runtime \ src \ coreclr \ src \ vm \ comutilnative.h # 112 и добавить:

static FCDECL0(UINT64, GetAllocationCount);

Чтобы связать эти два метода, нам нужно перечислить их в 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, где живет наш счетчик выделения. Метод 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() как внутренний вызов, как показано выше).

Закрытие

По общему признанию, это было бы совсем немного работы, если бы не было сделано раньше и пользовательский G C + CLR мог бы быть здесь немного излишним, но я подумал, что я должен выбросить его туда как возможность.

Кроме того, я не проверял это. Вы должны принять это как концепцию.

1 голос
/ 18 апреля 2020

Вы можете записать каждое распределение. Но ваша логика c для этого внутри вашего процесса несовершенна. . NET Ядро поддерживает в процессе сбора данных ETW, что также позволяет записывать все события распределения. См.

Начиная с. NET Core 2.2, события CoreCLR теперь можно использовать с помощью класса System.Diagnostics.Tracing.EventListener. Эти события описывают поведение таких сервисов времени выполнения, как G C, 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");
    }
}

Это должно давать, когда вы включаете G C evets (0x1) вместо -1 все G C Время паузы и события G C, которые вам понадобятся для самодиагностики.

Встроен механизм выборки выделения. NET Core и. NET Framework, так как возрасты позволяют распределять объекты выборки метрики на каждые до 5 allo c событий / с GC_Alloc_Low или 100 allo c событий / с GC_Alloc_High выделенного объекта. Кажется, нет способа получить все события распределения, но если вы прочитаете. NET Базовый код

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

Вы можете записать все распределения внутри. NET Процесс Core 2.1+, если присутствует сеанс ETW, в котором записаны данные профилирования распределения.

Пример:

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

Причина, по которой я сказал вам, что вы регистрируете c, несовершенна в том, что даже простая операция, такая как печать событий выделения в консоль, будет выделить объекты. Вы видите, где это закончится? Если вы хотите добиться того, чтобы полный путь кода был свободным для выделения, я думаю, это невозможно, потому что, по крайней мере, прослушиватель событий ETW должен распределять данные вашего события. Вы достигли цели, но разбили приложение. Поэтому я бы полагался на ETW и записывал данные извне или с помощью профилировщика, который по той же причине должен быть неуправляемым.

С ETW вы получаете все стеки распределения и информацию о типе, что вам нужно не только для отчета, но и для поиска фрагмента кода, вызывающего сбой. Существует еще кое-что о методе встраивания, но этого уже достаточно для SO сообщения, я думаю.

0 голосов
/ 13 апреля 2020

Вам нужно использовать какую-нибудь функцию kernel32, но это возможно !! :) Я не написал полный код, но я надеюсь, что вы чувствуете, как это сделать.

Во-первых, вам нужно все обработайте с помощью функции: Process.GetProcesses ссылка , затем вам нужно создать из нее снимок CreateToolhelp32Snapshot, поскольку для этого снимка не требуется «пауза G C», а после того, как вам нужно создать цикл для перечисления на весь блок памяти. Функция цикла инициализируется с помощью Heap32ListFirst и Heap32First и после того, как вы можете вызывать Heap32Next до ее успешного завершения.

И вы можете вызывать функцию kerner32, когда она объявляет в вашем коде так:

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

Вот пример c ++, но вы можете сделать то же самое после объявления функции CSharp: Обход списка кучи

Я знаю, что это нелегко, но простого пути нет. Кстати, если вы позвоните по номеру Toolhelp32ReadProcessMemory внутри l oop, вы сможете получить много полезной другой информации.


И я нашел пинвоук. net возможно, это поможет вам пинвоук. net

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...