11

If I'm simply going to do the following to see what called me,

var st = new StackTrace();
var callingMethod = st.GetFrame(1).GetMethod()

would it be cheaper to just get that specific frame?

var sf = new StackFrame(1);
var callingMethod = sf.GetMethod()

I tested with the code below, but I'm unsure if my methods are sound.


Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
  var method = new StackFrame(1, false);
}
sw.Stop();
Trace.WriteLine(sw.ElapsedMilliseconds);

sw = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
  var method = new StackTrace().GetFrame(1);
}
sw.Stop();
Trace.WriteLine(sw.ElapsedMilliseconds);

// Results
// StackFrame: 850
// StackTrace: 1334

Is my approach (and results) correct?

Edit

I'd use the Caller Information attributes, however, I'm stuck in .NET 3.5 for the time being.

AndreyAkinshin
  • 18,603
  • 29
  • 96
  • 155
Jeff B
  • 8,572
  • 17
  • 61
  • 140
  • 1
    And of course, the most performant code is to not run it at all ;) http://stackoverflow.com/a/1348853/945456 – Jeff B Sep 16 '13 at 20:33
  • 5
    If your intention is to just get the "calling" members name, you are better off letting the compiler do that for with the new [Caller Information](http://msdn.microsoft.com/en-us/library/hh534540.aspx) attributes, which are new in C# 5 and VB.NET 11. – vcsjones Sep 16 '13 at 20:35
  • Your testing methods seem ok, but I would probably test each statement 1'000'000 times, just to be sure. – Sam Sep 16 '13 at 20:45
  • My suspicion is - if you got some stackshots of this process, you might find it spending the dominant fraction of its time in `new` (and GC). – Mike Dunlavey Sep 16 '13 at 20:55
  • @vcsjones I forgot to mention the limiting factor: .NET 3.5. – Jeff B Sep 16 '13 at 21:07
  • 1
    @JeffBridgman you can still use the calling target attributes as long as you are using the C# 5 compiler, you just need to define them yourself. So if you are targeting .NET Framework 3.5, but are using Visual Studio 2012, you can still do this since it's a compiler features. Here's a blog that walks you through it: http://www.thomaslevesque.com/2012/06/13/using-c-5-caller-info-attributes-when-targeting-earlier-versions-of-the-net-framework/ – vcsjones Sep 16 '13 at 21:37

1 Answers1

8

See recommendations for the compilation the correct benchmark. You should use prime number of iterations (for suppress JIT Loop unwinding optimization), run benchmark in Release mode without debugging, use cache warmup, etc.

I added your example in BenchmarkDotNet, look to StackFrameProgram.cs:

public class StackFrameProgram
{
    private const int IterationCount = 100001;

    public void Run()
    {
        var competition = new BenchmarkCompetition();
        competition.AddTask("StackFrame", () => StackFrame());
        competition.AddTask("StackTrace", () => StackTrace());
        competition.Run();
    }

    private StackFrame StackFrame()
    {
        StackFrame method = null;
        for (int i = 0; i < IterationCount; i++)
            method = new StackFrame(1, false);
        return method;
    }

    private StackFrame StackTrace()
    {
        StackFrame method = null;
        for (int i = 0; i < IterationCount; i++)
            method = new StackTrace().GetFrame(1);
        return method;
    }
}

There is my result (Intel Core i7-3632QM CPU 2.20GHz):

x86, .NET 3.5:
StackFrame : 1035ms
StackTrace : 1619ms

x64, .NET 3.5:
StackFrame :  981ms
StackTrace : 1754ms

x86, .NET 4.0:
StackFrame :  735ms
StackTrace : 1150ms

x64, .NET 4.0:
StackFrame : 637ms
StackTrace : 880ms

Let's look inside:

public StackFrame.ctor(int skipFrames, bool fNeedFileInfo)
{
    this.InitMembers();
    this.BuildStackFrame(skipFrames, fNeedFileInfo);
}

private void StackFrame.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);
        }
    }
} 

public StackTrace.ctor()
{
    this.m_iNumOfFrames = 0;
    this.m_iMethodsToSkip = 0;
    this.CaptureStackTrace(0, false, null, null);
}

private void StackTrace.CaptureStackTrace(int iSkip, bool fNeedFileInfo, Thread targetThread, Exception e)
{
    this.m_iMethodsToSkip += iSkip;
    StackFrameHelper sfh = new StackFrameHelper(fNeedFileInfo, targetThread);
    GetStackFramesInternal(sfh, 0, e);
    this.m_iNumOfFrames = sfh.GetNumberOfFrames();
    if (this.m_iMethodsToSkip > this.m_iNumOfFrames)
    {
        this.m_iMethodsToSkip = this.m_iNumOfFrames;
    }
    if (this.m_iNumOfFrames != 0)
    {
        this.frames = new StackFrame[this.m_iNumOfFrames];
        for (int i = 0; i < this.m_iNumOfFrames; i++)
        {
            bool flag = true;
            bool flag2 = true;
            StackFrame frame = new StackFrame(flag, flag2);
            frame.SetMethodBase(sfh.GetMethodBase(i));
            frame.SetOffset(sfh.GetOffset(i));
            frame.SetILOffset(sfh.GetILOffset(i));
            frame.SetIsLastFrameFromForeignExceptionStackTrace(sfh.IsLastFrameFromForeignExceptionStackTrace(i));
            if (fNeedFileInfo)
            {
                frame.SetFileName(sfh.GetFilename(i));
                frame.SetLineNumber(sfh.GetLineNumber(i));
                frame.SetColumnNumber(sfh.GetColumnNumber(i));
            }
            this.frames[i] = frame;
        }
        if (e == null)
        {
            this.m_iMethodsToSkip += CalculateFramesToSkip(sfh, this.m_iNumOfFrames);
        }
        this.m_iNumOfFrames -= this.m_iMethodsToSkip;
        if (this.m_iNumOfFrames < 0)
        {
            this.m_iNumOfFrames = 0;
        }
    }
    else
    {
        this.frames = null;
    }
}

public virtual StackFrame StackTrace.GetFrame(int index)
{
    if (((this.frames != null) && (index < this.m_iNumOfFrames)) && (index >= 0))
    {
        return this.frames[index + this.m_iMethodsToSkip];
    }
    return null;
}
AndreyAkinshin
  • 18,603
  • 29
  • 96
  • 155
  • @JeffBridgman, It is sufficient that the number of iterations had no little dividers. I checked the asm code in my example, the loop unwinding optimization does not occur. – AndreyAkinshin Sep 17 '13 at 18:58
  • Good to know. Thank you for the thorough explanation. – Jeff B Sep 17 '13 at 19:32