4

我正在尝试编写一个记录进程中所有 .Net 方法调用的分析器。目标是使其具有高性能并保持假设最后 5-10 分钟在内存中(固定缓冲区,循环覆盖旧信息),直到用户触发将该信息写入磁盘。预期用途是跟踪很少重现的性能问题。

我从https://github.com/appneta/SimpleCLRProfiler的 SimpleCLRProfiler 项目开始。分析器使用 .Net 分析的ICorProfilerCallback2回调接口。我让它在我的环境中编译和工作(Win 8.1、.Net 4.5、VS2012)。但是,我注意到有时会丢失已记录 Enter 呼叫的 Leave 呼叫。Console.WriteLine 调用示例(我将 DbgView 的输出减少到理解的最低限度):

Line 1481: Entering System.Console.WriteLine
Line 1483: Entering SyncTextWriter.WriteLine
Line 1485: Entering System.IO.TextWriter.WriteLine
Line 1537: Leaving SyncTextWriter.WriteLine

两个进入呼叫没有对应的离开呼叫。分析后的 .Net 代码如下所示:

Console.WriteLine("Hello, Simple Profiler!");

相关的 SimpleCLRProfiler 方法是:

HRESULT CSimpleProfiler::registerGlobalCallbacks() 
{
   HRESULT hr = profilerInfo3->SetEnterLeaveFunctionHooks3WithInfo(
      (FunctionEnter3WithInfo*)MethodEntered3, 
      (FunctionEnter3WithInfo*)MethodLeft3, 
      (FunctionEnter3WithInfo*)MethodTailcall3);

   if (FAILED(hr))
      Trace_f(L"Failed to register global callbacks (%s)", _com_error(hr).ErrorMessage());

   return S_OK;
}

void CSimpleProfiler::OnEnterWithInfo(FunctionID functionId, COR_PRF_ELT_INFO eltInfo)
{
    MethodInfo info;
   HRESULT hr = info.Create(profilerInfo3, functionId);
   if (FAILED(hr)) 
      Trace_f(L"Enter() failed to create MethodInfo object (%s)", _com_error(hr).ErrorMessage());

   Trace_f(L"[%p] [%d] Entering %s.%s", functionId, GetCurrentThreadId(), info.className.c_str(), info.methodName.c_str());
}

void CSimpleProfiler::OnLeaveWithInfo(FunctionID functionId, COR_PRF_ELT_INFO eltInfo)
{
   MethodInfo info;
   HRESULT hr = info.Create(profilerInfo3, functionId);
   if (FAILED(hr)) 
      Trace_f(L"Enter() failed to create MethodInfo object (%s)", _com_error(hr).ErrorMessage());

   Trace_f(L"[%p] [%d] Leaving %s.%s", functionId, GetCurrentThreadId(), info.className.c_str(), info.methodName.c_str());
}

有谁知道,为什么 .Net Profiler 不会对所有离开方法执行离开调用?顺便说一句,我检查了 OnLeaveMethod 不会在任何跟踪之前由于异常左右而意外退出。它没有。

谢谢,克里斯托夫

4

1 回答 1

4

由于 stakx 似乎没有回到我的问题来提供官方答案(并获得荣誉),所以我会为他做这件事:正如 stakx 暗示的那样,我没有记录尾调用。事实上,我什至不知道这个概念,所以我完全忽略了那个钩子方法(它被连接起来但是空的)。我在这里找到了关于尾调用的一个很好的解释:David Broman 的 CLR Profiling API 博客:进入、离开、尾调用钩子第 2 部分:尾调用的传奇故事

我引用上面的链接:

尾调用是一种编译器优化,可以节省指令的执行和栈内存的读写。当一个函数做的最后一件事是调用另一个函数(并且其他条件有利)时,编译器可能会考虑将该调用实现为尾调用,而不是常规调用。

考虑这段代码:

static public void Main() {
    Helper();
}

static public void Helper() {
    One();
    Three();
}

static public void Three() {
    ...
}

当调用方法时,如果没有尾调用优化,堆栈将如下所示。

Three
Helper
Main

使用尾调用优化后,堆栈如下所示:

Three
Main

所以在调用Three之前,由于优化,方法Helper已经从堆栈中弹出,结果堆栈上的方法少了一个(更少的内存使用),并且还保存了一些执行和内存写入操作。

于 2015-06-23T02:28:36.463 回答