Как работает StackFrame?

Я рассматриваю возможность использования чего-то типа StackFrame stackFrame = new StackFrame(1) для регистрации метода выполнения, но я не знаю о его последствиях для производительности. Является ли трассировка стека тем, что строится в любом случае с каждым вызовом метода, поэтому производительность не должна быть проблемой или это что-то, что только создается, когда его просят? Вы рекомендуете против этого в приложении, где производительность очень важна? Если да, значит ли это, что я должен отключить его для выпуска?

Ответ 1

изменить: Некорректный


У нас есть аналогичная функция, которая отключена в 99% случаев; мы использовали такой подход, как:

public void DoSomething()
{
    TraceCall(MethodBase.GetCurrentMethod().Name);
    // Do Something
}

public void TraceCall(string methodName)
{
    if (!loggingEnabled) { return; }
    // Log...
}

TraceCall(MethodBase.GetCurrentMethod().Name)

Это было просто, но независимо от того, была ли включена трассировка, мы столкнулись с хитом производительности использования Reflection для поиска имени метода.

Наши варианты заключались в том, чтобы либо требовать больше кода в каждом методе (и рисковать простыми ошибками, либо отказом), либо переключиться на использование StackFrame, чтобы определить вызывающий метод только при включении ведения журнала.

Вариант A:

public void DoSomething()
{
    if (loggingEnabled)
    {
        TraceCall(MethodBase.GetCurrentMethod().Name);
    }
    // Do Something
}

public void TraceCall(string methodName)
{
    if (!loggingEnabled) { return; }
    // Log...
}

Вариант B:

public void DoSomething()
{
    TraceCall();
    // Do Something
}

public void TraceCall()
{
    if (!loggingEnabled) { return; }
    StackFrame stackFrame = new StackFrame(1);
    // Log...
}

Мы выбрали вариант B. Он предлагает значительные улучшения производительности по сравнению с опцией A , когда регистрация отключена, 99% времени и очень проста в реализации.

Здесь изменение кода Майкла, чтобы отобразить стоимость/выгоду этого подхода

using System;
using System.Diagnostics;
using System.Reflection;

namespace ConsoleApplication
{
    class Program
    {
        static bool traceCalls;

        static void Main(string[] args)
        {
            Stopwatch sw;

            // warm up
            for (int i = 0; i < 100000; i++)
            {
                TraceCall();
            }

            // call 100K times, tracing *disabled*, passing method name
            sw = Stopwatch.StartNew();
            traceCalls = false;
            for (int i = 0; i < 100000; i++)
            {
                TraceCall(MethodBase.GetCurrentMethod());
            }
            sw.Stop();
            Console.WriteLine("Tracing Disabled, passing Method Name: {0}ms"
                             , sw.ElapsedMilliseconds);

            // call 100K times, tracing *enabled*, passing method name
            sw = Stopwatch.StartNew();
            traceCalls = true;
            for (int i = 0; i < 100000; i++)
            {
                TraceCall(MethodBase.GetCurrentMethod());
            }
            sw.Stop();
            Console.WriteLine("Tracing Enabled, passing Method Name: {0}ms"
                             , sw.ElapsedMilliseconds);

            // call 100K times, tracing *disabled*, determining method name
            sw = Stopwatch.StartNew();
            traceCalls = false;
            for (int i = 0; i < 100000; i++)
            {
                TraceCall();
            }
            Console.WriteLine("Tracing Disabled, looking up Method Name: {0}ms"
                       , sw.ElapsedMilliseconds);

            // call 100K times, tracing *enabled*, determining method name
            sw = Stopwatch.StartNew();
            traceCalls = true;
            for (int i = 0; i < 100000; i++)
            {
                TraceCall();
            }
            Console.WriteLine("Tracing Enabled, looking up Method Name: {0}ms"
                       , sw.ElapsedMilliseconds);

            Console.ReadKey();
        }

        private static void TraceCall()
        {
            if (traceCalls)
            {
                StackFrame stackFrame = new StackFrame(1);
                TraceCall(stackFrame.GetMethod().Name);
            }
        }

        private static void TraceCall(MethodBase method)
        {
            if (traceCalls)
            {
                TraceCall(method.Name);
            }
        }

        private static void TraceCall(string methodName)
        {
            // Write to log
        }
    }
}

Результаты:

Tracing Disabled, passing Method Name: 294ms
Tracing Enabled,  passing Method Name: 298ms
Tracing Disabled, looking up Method Name: 0ms
Tracing Enabled,  looking up Method Name: 1230ms

Ответ 2

Быстрый и наивный тест показывает, что для кода, чувствительного к производительности, да, вы хотите обратить внимание на это:

Не создавайте кадры 100K: 3 мс

Генерировать 100K кадров: 1805 мс

Около 20 микросекунд на сгенерированный фрейм на моей машине. Не много, но измеримая разница в большом количестве итераций.

Говоря на ваши более поздние вопросы ( "Должен ли я отключать генерацию StackFrame в моем приложении?" ), я бы предложил вам проанализировать ваше приложение, выполнить тесты производительности, такие как те, которые я сделал здесь, и посмотреть, на что-либо с вашей рабочей нагрузкой.

using System;
using System.Diagnostics;

namespace ConsoleApplication
{
    class Program
    {
        static bool generateFrame;

        static void Main(string[] args)
        {
            Stopwatch sw;

            // warm up
            for (int i = 0; i < 100000; i++)
            {
                CallA();
            }

            // call 100K times; no stackframes
            sw = Stopwatch.StartNew();
            for (int i = 0; i < 100000; i++)
            {
                CallA();
            }
            sw.Stop();
            Console.WriteLine("Don't generate 100K frames: {0}ms"
                                 , sw.ElapsedMilliseconds);

            // call 100K times; generate stackframes
            generateFrame = true;
            sw = Stopwatch.StartNew();
            for (int i = 0; i < 100000; i++)
            {
                CallA();
            }
            Console.WriteLine("Generate 100K frames: {0}ms"
                           , sw.ElapsedMilliseconds);

            Console.ReadKey();
        }

        private static void CallA()
        {
            CallB();
        }

        private static void CallB()
        {
            CallC();
        }

        private static void CallC()
        {
            if (generateFrame)
            {
                StackFrame stackFrame = new StackFrame(1);
            }
        }
    }
}

Ответ 3

Я знаю, что это старый пост, но на всякий случай кто-то сталкивается с ним, есть еще одна альтернатива, если вы нацеливаетесь на .Net 4.5

Вы можете использовать атрибут CallerMemberName для определения имени вызывающего метода. Это намного быстрее, чем отражение или StackFrame. Вот результаты быстрого теста, повторяющегося миллион раз. StackFrame чрезвычайно медленный по сравнению с отражением, и новый атрибут делает оба похожими на то, что они стоят на месте. Это было запущено в среде IDE.

Результат отражения: 00: 00: 01,4098808

Результат StackFrame 00: 00: 06,2002501

Атрибут CallerMemberName Результат: 00: 00: 00,0042708

Готово

Ниже приведено скомпилированное exe: Результат отражения: 00: 00: 01,2136738 Результат StackFrame 00: 00: 03,6343924 Атрибут CallerMemberName Результат: 00: 00: 00,0000947 Готово

        static void Main(string[] args)
    {

        Stopwatch sw = new Stopwatch();

        sw.Stop();

        Console.WriteLine("Reflection Result:");

        sw.Start();
        for (int i = 0; i < 1000000; i++)
        {
            //Using reflection to get the current method name.
            PassedName(MethodBase.GetCurrentMethod().Name);
        }
        Console.WriteLine(sw.Elapsed);

        Console.WriteLine("StackFrame Result");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            UsingStackFrame();
        }

        Console.WriteLine(sw.Elapsed);

        Console.WriteLine("CallerMemberName attribute Result:");

        sw.Restart();
        for (int i = 0; i < 1000000; i++)
        {
            UsingCallerAttribute();
        }

        Console.WriteLine(sw.Elapsed);

        sw.Stop();



        Console.WriteLine("Done");
        Console.Read();
    }


    static void PassedName(string name)
    {

    }

    static void UsingStackFrame()
    {
        string name = new StackFrame(1).GetMethod().Name;
    }


    static void UsingCallerAttribute([CallerMemberName] string memberName = "")
    {

    }

Ответ 4

Из документации MSDN все время создается StackFrames:

Создается и нажимается StackFrame стек вызовов для каждого вызова функции выполненных во время выполнения потока. Рамка стека всегда включает Информация о методе базы данных и, необязательно, включает имя файла, номер строки и номер столбца.

Конструктор new StackFrame(1), который вы бы назвали, сделает это:

private void BuildStackFrame(int skipFrames, bool fNeedFileInfo)
{
    StackFrameHelper sfh = new StackFrameHelper(fNeedFileInfo, null);
    StackTrace.GetStackFramesInternal(sfh, 0, null);
    int numberOfFrames = sfh.GetNumberOfFrames();
    skipFrames += StackTrace.CalculateFramesToSkip(sfh, numberOfFrames);
    if ((numberOfFrames - skipFrames) > 0)
    {
        this.method = sfh.GetMethodBase(skipFrames);
        this.offset = sfh.GetOffset(skipFrames);
        this.ILOffset = sfh.GetILOffset(skipFrames);
        if (fNeedFileInfo)
        {
            this.strFileName = sfh.GetFilename(skipFrames);
            this.iLineNumber = sfh.GetLineNumber(skipFrames);
            this.iColumnNumber = sfh.GetColumnNumber(skipFrames);
        }
    }
}

GetStackFramesInternal - внешний метод. CalculateFramesToSkip имеет цикл, который работает ровно один раз, поскольку вы указали только 1 кадр. Все остальное выглядит довольно быстро.

Вы пытались измерить, сколько времени потребуется, чтобы создать, скажем, 1 миллион из них?

Ответ 5

Я рассматриваю возможность использования чего-то типа StackFrame stackFrame = new StackFrame (1) для регистрации метода выполнения

Из интереса: Почему? Если вам нужен только текущий метод, то

string methodName = System.Reflection.MethodBase.GetCurrentMethod().Name;

кажется лучше. Может быть, не более совершенный (я не сравнивал, но Reflection показывает, что GetCurrentMethod() не просто создает StackFrame, а делает некоторые "магии" ), но более ясный в этом намерении.

Ответ 6

Я думаю, что Пол Уильямс ударил ноготь по голове своей работой. Если вы углубитесь в StackFrameHelper, вы обнаружите, что fNeedFileInfo на самом деле является убийцей производительности, особенно в режиме отладки. Попробуйте установить значение false, если производительность важна. В любом случае вы не получите много полезной информации в режиме выпуска.

Если вы передадите false, вы все равно получите имена методов при выполнении ToString() и без вывода какой-либо информации, просто перемещая указатели стека вокруг, это очень быстро.

Ответ 7

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

if (loggingEnabled) TraceCall(MethodBase.GetCurrentMethod());  

Или, если вы хотите, чтобы TraceCall выполнялся всегда:

TraceCall(loggingEnabled ? MethodBase.GetCurrentMethod() : null);