【问题标题】:How performant is StackFrame?StackFrame 的性能如何?
【发布时间】:2010-11-23 20:01:27
【问题描述】:

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

【问题讨论】:

  • 是否始终启用此日志记录?什么是替代方法? Michael 的结果很有趣,但真正的问题是“这种方法如何达到 X?”
  • 除了在每条日志语句中手动写出方法名外,您还可以查看编译时面向方面的编程库,并挂钩到编译时事件以捕获方法名,以及记录它的运行时方面。 PostSharp 就是这样一个库。不过可能有点矫枉过正。
  • 另一种选择可能是使用 eXtensible C# 编写自定义编译器属性以静态生成日志调用
  • 对不起,但是,我不得不说...“性能”不是一个词。 ,感觉好多了:-)
  • @Hermann - “性能”这个词主要用于技术界,在这种情况下,您已经正确使用了它。很多说英语的人不喜欢这个词,我猜是因为它感觉很尴尬或太“管理”。大多数字典里都没有。有很多类似的词(“可操作”、“学习”),但我喜欢“性能”。它填补了一个空白。

标签: c# .net performance logging


【解决方案1】:

从 MSDN 文档看来,StackFrames 似乎一直在创建:

创建并推送 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 万个)需要多长时间?

【讨论】:

  • 谢谢保罗。我在 MSDN 中阅读了相同的段落,并试图追踪它在 Reflector 中的工作原理,但并没有真正理解它。我正在考虑计时,但如果在第一次调用后以某种方式缓存它可能会产生误导,这就是为什么我希望有人知道它是如何在内部工作的。
  • Michael 的测试表明堆栈帧不是“在线程执行期间进行的每个函数调用都创建并推送到调用堆栈上”,因为请求它确实需要更长的时间。
  • 或者他们可能以不同的方式创建它们,或者在请求之前它可能是本机构造,或者文档可能是错误的。我假设 CLR 需要跟踪托管堆栈,因此随着堆栈的增长,本机内存设置中必须有 something
  • 记住“堆栈帧”不一定是“堆栈帧”
【解决方案2】:

一个快速而幼稚的测试表明,对于性能敏感的代码,是的,你要注意这一点:

不生成 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);
            }
        }
    }
}

【讨论】:

  • 谢谢迈克尔!我喜欢你测试这个的方式,因为我担心 .NET 框架会以某种方式缓存该方法,但你的测试似乎表明情况并非如此。
  • 是的,我不确定如何将 Paul 引用的文章与我的经验和测试相匹配。我也一直在浏览 log4net 文档,该文档明确调用了生成堆栈帧(用于包含在日志中)的速度。
  • 我重复了这个测试,将 [MethodImpl(MethodImplOptions.NoInlining)] 应用于每个方法,以防万一,我还为每个具有 100k 次迭代的 StackFrame 获得了大约 20µs,对于每个具有 100 万次迭代的 StackFrame,我得到了大约 15µs。 15-20 微秒似乎并不算太​​糟糕。毕竟创建 50 个 StackFrame 只需要 1 毫秒。
  • 我也不知道。可能是框架调用了 new StackFrame(),它调用了 BuildStackFrame(0, false)。或者文档可能具有误导性。 /shrug +1 实际测试并报告结果!
【解决方案3】:

编辑:一些背景


我们有一个类似的功能,它在 99% 的情况下都被禁用;我们使用的方法如下:

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

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

TraceCall(MethodBase.GetCurrentMethod().Name)

这很简单,但无论是否启用跟踪,我们都会在使用反射查找方法名称时产生性能损失。

我们的选择是,要么在每个方法中要求更多代码(并冒着简单错误或被拒绝的风险),要么在启用日志记录时改用StackFrame 来确定调用方法。 p>

选项 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。在 99% 的情况下禁用日志记录,它比选项 A 提供了显着的性能改进,并且非常易于实施。 p>

这是对 Michael 代码的修改,以显示这种方法的成本/收益

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

【讨论】:

  • 优点:通常,您可以退后一步,找到解决问题的另一种方法。如果您永远不需要调用 new StackFrame() ,它并不昂贵。
  • @Michael:没错!它实现起来非常干净,易于进行单元测试,并且我们唯一会受到性能影响的时候是我们基本上是在调试并且无论如何性能都不是问题。
  • 我想这取决于您启用跟踪的频率,因为如果启用了跟踪,选项 B 仍然比选项 A 慢 10 倍。但是现在每个人都可以根据事实自行决定。很有见地。谢谢!
  • 我只是尝试在没有调试模式的情况下启动应用程序,并且关于使用堆栈帧的数字发生了显着变化。在 Visual Studio 中调试需要 1117 毫秒,从调试编译运行程序集需要 729 毫秒,从发布编译运行程序集需要 569 毫秒。其他数字没有变化。最后使用 stackframe 在发布模式下仅慢 3 倍,这意味着最终产品。
  • 嗯...它假定您将在一个子函数中进行跟踪,并且您将在那里查看跟踪标志。这是一种常见的情况,但是如果您不进行子调用(在调用 getcurrentmethod 之前查看跟踪标志),那么 getcurrentmethod 将获得与不被调用相同的好处,并且会比 StackFrame 更快。它只对跟踪有意义,如果你需要这个东西,getcurrentmethod 更快更简单。
【解决方案4】:

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

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

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

似乎更好。可能性能不高(我没有比较,但 Reflection 表明 GetCurrentMethod() 不仅创建了一个 StackFrame,还做了一些“魔术”),但它的意图更清晰。

【讨论】:

  • 我用 MethodBase.GetCurrentMethod() 尝试了 Michael 的测试,你是对的,它实际上更快。在我的机器上每次调用大约需要 1 微秒。
【解决方案5】:

我认为 Paul Williams 在完成工作时一针见血。如果您深入研究StackFrameHelper,您会发现fNeedFileInfo 实际上是性能杀手——尤其是在调试模式下。如果性能很重要,请尝试将其设置为 false。无论如何,在发布模式下你不会得到太多有用的信息。

如果您在此处传递 false,则在执行 ToString() 时仍然会获得方法名称,并且不输出任何信息,只是移动堆栈指针,速度非常快。

【讨论】:

    【解决方案6】:

    我知道你的意思,但是这个示例结果是过冲的。即使在关闭日志记录的情况下执行 GetCurrentMethod 也是一种浪费。 它应该是这样的:

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

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

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

    【讨论】:

      【解决方案7】:

      我知道这是一篇旧帖子,但万一有人看到它,如果您的目标是 .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 堆栈帧结果 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 = "")
          {
      
          }
      

      【讨论】:

      • 这个属性更快的原因是因为编译器实际上直接将它刻录到生成的图像中。在调用使用编译器服务属性的方法的所有地方,值将替换为编译器应该提供的任何内容(CallerMemberName、CallerFileName、CallerLineNumber)。它基本上是二进制重写。这可以使用 ILDASM 工具来确认。
      • 这也没有得到调用者的类型,这显然只能通过堆栈框架获得,并且是找出某些东西来自哪里的重要部分。
      • 您不一定需要调用者类型,因为您可以从[CallerFilePath][CallerLineNumber] 属性有效地获取相同(如果不是更多)信息。最烦人的部分是它需要一个带有默认值的可选参数,所以如果你的日志记录函数需要一个Stringparams object[] 来传递给String.Format,你就不能使用它们。
      猜你喜欢
      • 2016-09-13
      • 1970-01-01
      • 2011-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多