StackFrame的表现如何?

时间:2009-08-28 18:31:15

标签: c# .net performance logging

我正在考虑使用StackFrame stackFrame = new StackFrame(1)之类的东西来记录执行方法,但我不知道它的性能影响。堆栈是否跟踪了每个方法调用时构建的内容,因此性能不应该是一个问题,还是仅在被要求时才构建?您是否建议在性能非常重要的应用程序中反对它?如果是这样,这是否意味着我应该在发布时禁用它?

7 个答案:

答案 0 :(得分:31)

编辑一些背景


我们有一个类似的功能,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

答案 1 :(得分:18)

快速而天真的测试表明,对于对性能敏感的代码,是的,您需要注意这一点:

  

不要生成100K帧:3ms

     

生成100K帧:1805ms

在我的机器上每个生成的帧约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);
            }
        }
    }
}

答案 2 :(得分:17)

我知道这是一篇旧帖子,但万一有人遇到它,如果你的目标是.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 = "")
    {

    }

答案 3 :(得分:5)

从MSDN文档中,似乎一直在创建StackFrame:

  

创建并推送StackFrame   每个函数调用的调用堆栈   在执行一个线程时做的。   堆栈帧始终包括   MethodBase信息,也可以选择   包括文件名,行号和   列号信息。

您调用的构造函数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帧。其他一切看起来都很快。

您是否尝试过测量创建100万个片段需要多长时间?

答案 4 :(得分:4)

  

我正在考虑使用类似StackFrame的StackFrame = new StackFrame(1)来记录执行方法

出于兴趣:为什么?如果您只想要当前的方法,那么

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

似乎更好。也许不是更高性能(我没有比较,但是Reflection显示GetCurrentMethod()不是简单地创建一个StackFrame而是做一些“魔术”),但是它的意图更清晰。

答案 5 :(得分:3)

我认为保罗·威廉姆斯的工作正在进行中。如果你深入研究StackFrameHelper,你会发现fNeedFileInfo实际上是性能杀手 - 特别是在调试模式下。如果性能很重要,请尝试将其设置为 false 。无论如何,你不会在发布模式中获得更多有用的信息。

如果您在此处传递 false ,那么在执行ToString()时仍然会获得方法名称而不输出任何信息,只需移动堆栈指针,它就会非常快。

答案 6 :(得分:2)

我知道你的意思,但这个例子的结果是超调。即使关闭日志记录也执行GetCurrentMethod是一种浪费。 它应该是这样的:

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

或者,如果您希望TraceCall始终执行:

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