C++ 的基本仪器和剖析框架





5.00/5 (14投票s)
为 C++ 应用程序编写的基于宏的框架,它将提供基本的仪器和剖析功能。

引言
我开发基于 Windows CE 设备的应用程序大约五年了。在这五年中,该平台的开发工具质量有了显著提高。然而,与 Windows 桌面开发人员可用的工具相比,其工具质量仍然相形见绌。这些移动设备虽然有调试器,但并非总是能够使用。很多时候,调试应用程序时,开发人员只能使用 MessageBox 或将数据写入日志文件。
在我协助开发的一些项目中,开发人员创建了一系列 C 预处理器宏来检测他们的代码。当需要检测报告时,这些宏可以有选择地编译到应用程序中。通常,输出会写入日志文件。通常会创建一个宏来指示函数何时进入、何时离开、输入参数和返回值,甚至还有用于代码中某些检查点的报告宏。
我喜欢这种报告提供的详细输出,然而,我遇到的几乎所有宏方案都同样冗长。这些宏条目经常会干扰代码的可读性,填写起来很麻烦。它们使用 printf
机制报告数据值,并且由于 C 预处理器无法重载宏,参数报告宏通常会为要报告的参数数量定义多个版本。名称通常以 "_x" 结尾,其中 x 是要报告的参数数量。这些基本解决方案通常也不是线程安全的,并且由于文件系统的 IO 操作而严重影响应用程序的性能。
我决定尝试利用 C++ 的强大功能,创建一个具有以下特性的检测框架:
- 检测代码的工作量极小
- 语法简洁
- 自动报告所有可通过编译器确定的信息
- 可移植到 Windows Desktop 和 Windows CE 的 C++ 编译器
- 简单的消息语法
- 线程安全
- 利用堆栈自动检测作用域级别
- 可配置的报告级别
- 消除繁琐的宏条目
- 对应用程序性能影响最小
- 可能移植到其他平台
本文档记录了我开发的框架。我能够实现上述大部分目标。其中一些目标只是对启发该框架的代码进行了一些改进。然而,对于大多数目标,我相信我已经创建了一个更好的模型,可以轻松地检测代码。
在此过程中,我发现我不仅可以对代码进行基本输出日志记录,还可以以极小的额外工作量包含一个基本的性能分析框架。以下是该框架提供的性能分析功能列表:
- 函数入口计数
- 函数使用时间
- 函数总使用量
- 暂停和恢复使用时钟以考虑系统阻塞操作的能力
框架
我们首先为那些对使用此代码而非其工作原理更感兴趣的读者描述该框架。该系统包含在一个名为 FnTrace.h 的头文件中。大约有十到十五个宏,旨在供开发人员使用以协助代码实现。以下是这些命令的名称:
FN_CHECKPOINT
FN_ENABLE_PROFILE
FN_INSTRUMENT_LEVEL
FN_INSTRUMENT
FN_INSTRUMENT_THREAD
FN_ENTRY
FN_FUNC
FN_FUNC_RETVAL
FN_FUNC_PARAMS
FN_OUTPUT
FN_PARAM
FN_PAUSE
FN_RESUME
FN_RETVAL
这些宏各司其职,并非都需要使用。宏的选择将基于开发人员希望实现的目标。
要求
本节描述了 FnTrace
框架的基本要求:
- Visual Studio 2005 或更高版本以获得最佳效果
- Boost 预处理器库
- C++ 源代码
用法
本节将描述为应用程序启用函数检测所需的操作。所有旨在直接使用的宏也将被记录和解释。
激活检测
为了激活检测,需要在文件包含 FnTrace
头文件之前定义以下宏。如果您正在使用预编译头文件,我建议您将头文件添加到预编译头文件中。
#define FN_INSTRUMENT
声明默认日志级别(可选)
您可以为您的应用程序声明所需的检测日志级别。如果您不定义这些日志级别,将使用默认级别 FN_5
,这是最大详细级别。还有一个将在稍后描述的宏,可以调用它以在运行时更改日志级别。
#define FN_FUNC_LEVEL_DEF FN_3
FN_FUNC_LEVEL_DEF:
此 MACRO 定义报告函数级别操作的默认日志级别。这包括函数进入和退出、性能分析和一般输出语句。如果日志级别低于为函数定义的级别,则该函数中找到的语句将不会报告到检测日志。
#define FN_PARAM_LEVEL_DEF FN_5
FN_PARAM_LEVEL_DEF:
此 MACRO 定义报告函数参数值的默认日志级别。如果日志级别低于为函数定义的级别,则该函数中找到的参数输出语句将不会报告到检测日志。如果此日志级别设置为低于函数日志级别的值,它将自动移动到与 FN_FUNC_LEVEL_DEF
值相同的级别。
#Include "FnTrace.h"
在您打算检测的每个文件中包含 FN Trace 框架头文件。如果您正在使用预编译头文件,我建议您将头文件添加到预编译头文件中。
在运行时更改检测级别
本节中的 MACRO 可用于在运行时动态更改检测级别。这可以附加到 UI 选项或您主窗口 WndProc
中的消息处理器的一部分。
FN_INSTRUMENT_LEVEL(F,P);
F
: 新的函数跟踪级别,FN_0
到FN_5
P
: 新的参数跟踪级别,FN_0
到FN_5
初始化线程检测
对于将要检测的每个线程,需要在线程函数开头定义以下条目,以初始化框架的线程状态:
FN_INSTRUMENT_THREAD;
启用性能分析(可选)
如果需要函数性能分析,请在 main
函数顶部声明以下 MACRO。
FN_ENABLE_PROFILE(1);
您可以通过调用相同的宏并将其值设置为 0
,在程序稍后运行时关闭函数性能分析。
FN_ENABLE_PROFILE(0);
启用函数检测
对于您希望在应用程序中检测的每个函数,请在函数作用域的大括号内顶部添加以下 MACRO
FN_FUNC(retType, level);
ex:
bool IsEmpty()
{
FN_FUNC(bool, FN_1);
...
}
retType
: 函数返回值的类型。对于返回类型为void
的函数,使用FN_VOID
定义作为返回类型。level
: 此函数的跟踪级别。
通知框架返回值
FN 框架能够在函数退出时报告函数返回的值。为了启用此功能,需要遵循编程约定。您需要声明本节中的宏,并为返回值分配空间。返回变量的名称传递给宏,在函数退出时,此变量应该在 return
调用中。
FN_RETVAL(retVar);
ex:
bool IsEmpty()
{
...
bool retVal = false;
FN_RETVAL(retVal);
...
retVal = true;
return retVal;
}
retVar
: 函数退出时将返回的变量的名称。
启用带返回值的函数检测
本节是前两个宏的组合。可以定义此宏,并使其成为一步式过程。
FN_FUNC_RETVAL(retType, retVar, level);
ex:
bool IsEmpty()
{
bool retVal = false;
FN_FUNC_RETVAL(bool, retVal, FN_1);
...
retVal = true;
return retVal;
}
retType
: 函数的return
值的类型。对于具有void return
类型的函数,使用FN_VOID
定义作为返回类型。retVar
: 函数退出时将返回的变量的名称。level
: 此函数的跟踪级别。
启用完整函数检测
对于您希望在应用程序中检测的每个函数,请在大括号后的函数作用域顶部声明本节中的 MACRO。此宏将初始化适当的状态,以报告函数名称、您指示的每个参数,并在函数退出时报告返回值。如果您已为框架启用性能分析,此 MACRO 还将启动此函数的性能分析。
为了使用此版本的 MACRO,您需要 Visual Studio 2005 或更高版本,因为可变参数预处理器运算符 ...
FN_FUNC_PARAMS(retType, retVar, level, n, ...);
ex:
BOOL SetRect(LPRECT lprc, int xLeft, int yTop, int xRight, int yBottom)
{
BOOL retVal = FALSE;
FN_FUNC_RETVAL(BOOL, retVal, FN_1, 5, lprc, xLeft, yTop, xRight, yBottom);
...
retVal = TRUE;
return retVal;
}
retType
: 函数的return
值的类型。对于具有void return
类型的函数,使用FN_VOID
定义作为返回类型。retVar
: 函数退出时将返回的变量的名称。level
: 此函数的跟踪级别。n
: 要报告的参数数量。...
: 以逗号分隔列表形式报告的所有参数。
只要有可转换为其类型的 ostream operator<<
,就可以报告任何参数类型。因此,在上面的示例中,应用程序中的某个地方必须为以下 RECT
结构声明一个定义:
// Sample OStream implementation for the pointer to RECT struct
inline std::wostream& operator<<(std::wostream& os, const RECT* _Val)
{
if (_Val)
{
os << "RECT(" << _Val;
os << "): left " << _Val->left;
os << ", top " << _Val->top;
os << ", right " << _Val->right;
os << ", bottom " << _Val->bottom;
}
else
{
//C: A Null pointer was passed in.
os << L"(null);
}
return os;
}
// output:
// RECT(0034EF5A): left 0, top 0, right 640, bottom 480
在函数作用域中报告消息
使用 FN_OUTPUT
MACRO 将任何消息报告到当前函数作用域中的检测日志。FnTrace
框架的输出机制基于 C++ ostream
对象。输出 string
s 的形成方式与您写入 cout
消息的方式相同。输出的默认路径报告到 OutputDebugString
,可以由您的调试器或在您的机器上运行的侦听调试流的外部应用程序捕获。
FN_OUTPUT(output);
ex:
TCHAR windowName[100];
::_tcscpy(windowName, _T("Test Application"));
...
FN_OUTPUT(L"The current window name is: " << windowName);
output
: 以ostream
格式报告到检测日志的消息。
通过在包含 FnTrace
头文件之前声明此宏,可以将 output
流从 OutputDebugString
重定向到 std::clog
。
#define FN_OUTPUT_CLOG
参数报告
您可以使用以下宏报告单个变量或函数参数的名称和值。
FN_PARAM(param);
ex:
int x = 100;
FN_PARAM(x);
//result:
//x = 100
param:
要显示输出的变量。
选择性函数检测
有些函数被调用的频率很高,如果记录函数中的每一个入口,很快就会填满缓冲区。在这种情况下,您将有太多的信息,并且由于数据量太大,如果没有自动化工具,筛选起来会很困难,因此检测您的应用程序将不再有益。
一个完美的例子是 WIN32 API 中用户定义窗口的 WndProc
函数。大多数情况下调用此函数时,默认的 Windows 消息处理程序是完全可以接受的。在这种特定情况下,您很可能不关心此函数的入口。但是,您可能仍然对在 WndProc
函数中达到某些点感兴趣,例如当用户触发您已处理的 WM_COMMAND
消息时。
接下来的两个宏旨在结合使用,以提供函数的选择性检测。第一个宏将为检测准备函数作用域,第二个宏是检测检查点。如果达到检查点,则会打印出数据。如果函数被输入,但从未达到检查点,则不会为该函数报告任何数据。
FN_ENTRY
: 在FN_FUNC_x
类宏的位置使用此 MACRO。此 MACRO 将声明函数状态。
FN_ENTRY(level);
level
: 此函数的跟踪级别。FN_CHECKPOINT
: 使用此 MACRO 在当前函数作用域中声明一个检查点。
FN_ENTRY
MACRO 必须在此 MACRO 之前在当前函数作用域的某个点声明,否则会报告编译器错误。
FN_CHECKPOINT(label, level, n, ...);
ex:
...
case IDM_HELP_ABOUT:
{
FN_CHECKPOINT("IDM_HELP_ABOUT", FN_5, 4, hWnd, message, wParam, lParam);
DialogBox(g_hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, About);
}
break;
...
label
: 此检查点要报告的标签。level
: 此函数的跟踪级别。n
: 要报告的参数数量。...
: 以逗号分隔列表形式报告的所有参数。
只要有可转换为其类型的 ostream operator<<
,就可以报告任何参数类型。有关更多详细信息,请参阅 FN_FUNC_PARAMS
MACRO。
暂停和恢复性能分析计时
如果启用了函数性能分析,此宏可用于停止和启动函数的计时器。当函数调用长时间阻塞的内核函数(例如 GetMessage
或 WaitForSingleObject
)时,这可能很有用。
FN_PAUSE();
FN_RESUME();
ex:
// Main message loop:
FN_PAUSE();
while (GetMessage(&msg, NULL, 0, 0))
{
FN_RESUME();
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
FN_PAUSE();
}
实现
FnTrace
框架围绕一组对象构建,这些对象在进入跟踪范围时在堆栈上创建。对象的构造函数将记录重要的启动状态,析构函数将在函数退出之前报告退出状态。创建了三种类型的对象来促进该框架。我将在本节后面简要描述每个对象及其目的。
所有对象定义和全局变量都在 namespace fn{...}
中声明,以防止全局命名空间混乱。这一事实对于使用 FnTrace
框架的开发人员来说应该是完全透明的,因为所有访问都通过宏完成。不应直接调用任何对象。
以下小节将记录我实现当前版本的基本路径。我将指出我最初的设计动机,哪些可能有效,哪些失败了,我为什么改变它,以及我将指出框架中每个主要组件的最终解决方案。
消息报告
在开发框架时,我在宏中使用了 cout
,以快速启动和运行框架。我的意图是编写一个消息队列或数据包对象,将调用传输到不同的进程。然后,该外部进程可以处理消息的同步以及最终存储到文件或其他介质中进行分析。消息队列系统也将跨平台可移植,从 Windows CE 和 Windows Desktop 开始,但将继续移植到 Linux 和其他平台。
当我开发到创建这个对象时,我记起了 Win32 函数 OutputDebugString
。这个函数可以用来向附加到应用程序的调试器报告消息。将来我可能会创建那个可移植的消息队列类,但现在,通过使用 OutputDebugString
,我可以忽略一整段代码,而我只使用 WIN32 API 进行开发。
唯一的缺点是,我需要让调试器在应用程序运行时附加到我的应用程序上,以侦听调试字符串。好消息是,您的系统上有很多调试工具,它们实际上不是调试器,而是侦听应用程序的 OutputDebugString
数据的工具。一个这样的例子是 Windows SysInternals 的 Debug View。
要实现此功能,唯一的挑战是为每个线程创建一个 std::wostringstream
对象,以记录线程的状态。然后,当数据写入 OutputDebugString
函数时,必须清空 ostream
的读取缓冲区,或者将其设置回开头。有关更多详细信息,请查看 FN_OUTPUT
MACRO 的定义。
为了考虑可移植性,我还使输出能够重定向到 std::clog
。然而,由于我尚未用到此功能,因此尚未努力使其路径线程安全。
函数名
我对这个框架最初的设想是找到一种方法来声明一个宏,该宏可以在编译时从函数声明中或在程序的反汇编的幕后提取所需的所有信息。我不喜欢我所见过的所有基本检测宏实现都需要开发人员手动输入函数名称的事实。这使得检测应用程序变得繁琐且容易出错。如果你剪切和粘贴,很容易在第一次检测时忘记填写正确的函数名称,或者如果函数名称改变,就需要在函数中使用检测宏的每个地方修改名称。
我的第一个想法是使用编译器定义的宏 __FILE__
和 __LINE__
分别报告当前文件名和行号。我已经很熟悉这些宏了,因为我自己以及许多其他开发人员都使用可点击的 REMINDER MACRO。我的意图是编写一个分析工具,该工具将使用文件名和行号以及编译器生成的映射文件,并创建一个漂亮的 UI,以便更轻松地分析检测跟踪。这样做的好处是,我可以编写简短的神秘消息来指示状态,并且可以在分析时进行后处理,以期减少日志大小和在应用程序执行时对应用程序的影响。
我放弃这个想法的第一个原因是,那是我必须编写的另一段软件。可悲的是,这并不是我没有走这条路线的原因。真正的原因是我发现了一些 Microsoft 特定的预处理器宏,即 __FUNCSIG__
。这个宏报告了函数的名称,以及调用约定、返回类型和函数的参数类型。我不再需要编写解码工具了。我拥有提取函数名称、返回类型和所有参数所需的所有信息。
返回值
我到现在为止不喜欢检测宏的另一个方面是,为了报告函数的 return
值,必须在函数中的每个返回点报告这样一个宏:
...
INSTRUMENT_OUT_1("FunctionA: returns %s", name);
return name;
}
同样,函数的名称必须输入,需要一个笨重的 printf
格式字符串,并且这是在每个返回点都需要复制的宏。如果遗漏了其中一个点,或者在以后添加了,函数的退出就会被遗漏。
为了解决这个问题,我在函数入口处在堆栈上创建了一个对象。在这个对象的析构函数中,我打算在堆栈或函数寄存器中搜索 return
值,并报告它。我首先查看了 EAX 寄存器,这是 x86 架构中函数返回值通常存放的寄存器。我很快发现这行不通,因为析构函数本身与我正在检测的函数处于不同的堆栈帧中,并且 EAX 寄存器当前与所需的返回值无关,我无法找到一种可靠的方法来遍历堆栈以获取此值。这将使这个解决方案非常脆弱且不可移植。我还必须为我在 Windows CE 平台上运行的每个架构编写汇编代码。
解决此问题的一个中间方案是使用重新定义 return
关键字的 MACRO。这显然行不通,所以我尝试了一个诸如 RETURN(retVal)
这样的 MACRO。在 MACRO 内部,变量 retVal
将被赋值给存储在函数实现对象中的 retval
,并在调用析构函数时打印出来。开发人员需要在所有从函数调用返回的地方使用此宏。这仍然容易出错,并且看起来有点奇怪。
我的最终解决方案是预先声明一个 return
变量,并将该值的指针存储在我的函数堆栈对象中。然后,当开发人员将 return
值赋给 return
变量时,我就可以在我的堆栈对象中访问该值。这个解决方案要求用户遵循一个约定,然而,对于一个巨大的回报来说,这是一个很小的代价。
bool retVal = false;
FN_RETVAL(retVal);
...
if (x < minVal)
{
return retVal;
}
...
retVal = true;
return retVal;
}
参数值
我可能大部分开发时间都花在了寻找一种简单而健壮的方法来报告函数调用的参数值上。我真的不喜欢大多数 TRACE
宏以以下形式定义的方式:
#define TRACE_1(msg, p0)
#define TRACE_2(msg, p0, p1)
#define TRACE_3(msg, p0, p1, p2)
#define TRACE_4(msg, p0, p1, p2, p3)
#define TRACE_5(msg, p0, p1, p2, p3, p4)
#define TRACE_6(msg, p0, p1, p2, p3, p4, p5)
#define TRACE_7(msg, p0, p1, p2, p3, p4, p5, p6)
为了启动这个过程,我声明了这个宏:
#define FN_PARAM(param) FN_OUTPUT(#param << " = " << ##param##)
这要求为每个要报告的参数定义一个 FN_PARAM
,但对我来说,它比上面那些带有 printf
格式字符串的多个编号宏感觉更好。然而,它仍然很笨重,而且非常冗长,比我试图避免的方法糟糕得多。这个宏仍然存在于框架中,但它只是作为一个便利而保留,其他一些宏实际上会调用这个宏来完成它们的工作。
我开始研究堆栈帧汇编,并查看了 x86 基指针 EBP(也称为帧指针)。从这里,我可以向上遍历堆栈,第一个参数将是 EBP-8h
,第二个参数是 EBP-12h
,依此类推。当我开始沿着这条路径前进时,我很快意识到我将回到 printf
的困境,因为我将不再在编译时拥有类型信息。我将不得不做一些额外的处理以在运行时发现类型。
我已经在用 __FUNCSIG__
编译器宏获取函数定义了,所以我认为我会继续使用函数定义中定义的值来解码类型并从该路径生成输出。此时,我决定在函数声明宏中在堆栈上添加第二个对象,只是这个第二个对象将是 static
。这将允许该对象只在函数第一次调用时创建一次。这样,它可以保存我为函数计算的所有类型数据,我只需要做一次。事实证明,这比我最初想象的要困难得多。然而,这仍然是一个巨大的进步,因为它带来了框架现在提供的额外的性能分析功能。
在意识到自动堆栈方法的不足之后,我思考了其他可能出现的问题。自动尝试获取参数的一个主要问题是,很多时候参数可能是一个“仅输出”参数。这意味着进入函数的数据是无效的,并且报告到日志中是无用的。当这个新因素与之前类型信息已丢失的事实结合在一起时,在我看来,这种新方法的工作量太大,而收益却很小。
这条路径的最后一步是发现 Boost 预处理器库。这个库是一组非常酷的扩展预处理器宏。有些宏相当复杂,我花了一些时间仔细阅读文档并找到我实际需要的几个。我选择了基于 ARRAY
的宏,它们允许我将 variadic
条目转换为 ARRAY
。然后,我能够使用 Boost 的控制和循环定义来枚举我在宏中定义为输出的每个参数。
讽刺的是,我决定提供最初启发此框架的繁琐形式的宏,但仅是为了帮助支持不支持 Visual Studio 2005 中添加的可变参数预处理器 MACRO ...
的编译器。此 MACRO 就像 C++ 中可用于其函数中的宏一样。
线程安全
通过线程局部存储(TLS)为每个线程分配一组变量来实现线程安全。Windows 桌面编译器中的 TLS 机制比我为 Windows CE 对应版本添加支持所采取的路径简单得多。Windows 桌面支持编译器定义的 __declspec(thread)
定义,用于为创建的每个线程分配变量空间。目前这很方便,但是,这种形式确实有局限性,而且由于我不得不花功夫支持 CE,我可能会将 CE 方法作为我 Windows 实现中唯一使用的方法。我的决定将基于我在详细分析框架后对性能的影响。
对于基于 Windows CE 的系统,我不得不使用旨在提供 TLS 支持的 WIN32 API。
TLSAlloc
TLSGetValue
TLSSetValue
TLSFree
目前,在框架的 Windows 实现中,为每个线程分配了四个值:
bool isInstrumentThread
: 当创建线程时初始化此值。当定义FN_INSTRUMENT_THREAD
宏时,该值设置为true
。unsigned long indentLevel
: 此值表示活动线程的当前缩进级别。当每个函数进入堆栈时,缩进级别会递增,以便在检测日志中更容易查看调用堆栈。std::wostringstream *pInstLog
: 当使用默认输出方法时,此ostream
对象被初始化以帮助将流数据转换为输出。选择此转换对象是因为其易用性以及保留参数类型信息的能力。std::stack<timeEdge> *pTimeStack
: 创建此对象是为了帮助框架提供的函数性能分析。此参数将在下一节中进一步详细介绍。
这些参数中的每一个都在线程创建宏期间有条件地初始化,具体取决于编译器和其他 #define
。由于 TLS 存储的性质,对象需要作为指针存储。当线程退出时,线程对象析构函数将释放这些变量的内存。
已创建并使用了一系列辅助宏,以尽可能抽象地访问 TLS 变量,以期减少访问这些变量对使用此框架进行检测的程序的整体性能的影响。
分析
性能分析实际上是作为后话添加的,因为所有部分都已就位,再添加此功能不会花费太多精力。目前,性能分析功能非常原始,提供关于应用程序的详细信息非常有限。事实上,计时器分辨率以毫秒报告,因此计时功能的实际用途非常有限。因此,在这一点上,此功能实际上是关于概念验证。所有性能分析功能目前都由一个 static
模板对象促进,该对象在函数第一次调用时为每个函数创建。
函数使用计数
第一个相当容易实现的功能是计算函数被进入的次数。这是通过在每个函数中的 static
函数对象中递增计数器来完成的。此功能没有其他内容。了解特定函数被调用了多少次有什么价值?
- 指明哪些函数将受益于转换为内联或优化。
- 在函数调用次数多于或少于预期的情况下,提供对问题的深入了解。
函数执行时间
此功能跟踪 CPU 在特定函数作用域内花费的总时间。更准确地说,是在堆栈上存在每个特定函数实例的本地函数状态对象的时间量。此功能完成并使其完全有用有点棘手。主要问题是单个函数调用可能会调用其他函数。在这些“被调用者”函数中花费的时间不一定计入“调用者”的执行时间。如果不是这样,您的应用程序的 main
函数将花费最多的时间,当您将程序的总运行时间相加时,它将大于程序中实际花费的时间。
我的第一次尝试是简单地在 TLS 中创建一个堆栈对象,这将允许将每个函数的开始时间压入堆栈。然后,当函数退出时,栈顶的项将被弹出并从当前时间中减去。这将指示当前函数的总使用量。这时我意识到上面描述的第一个问题。堆栈中每个函数的使用量持续报告的值远大于我的预期。
我需要做的是创建一个机制来跟踪堆栈中更深层每个函数的使用情况。然后,当到达函数末尾时,我将从当前函数的当前执行时间中减去在堆栈中更深层花费的总时间。这个概念在我第一次开始时看起来相当直接。为了促进这一点,我将堆栈对象更改为使用 STL pair
。这使我能够将两个项耦合起来,即函数调用的开始时间和总延迟。开始时间在堆栈上建立,延迟通过 TLS 堆栈从“被调用者”传递给“调用者”。
最后一个要解决的问题是允许性能分析考虑此框架未实际管理的函数调用,例如阻塞调用或调用耗时库。为了让开发人员对计时检测有所控制,我添加了 FN_PAUSE
和 FN_RESUME
宏,以围绕 WaitForSingleObject
等调用放置。这很容易通过使用已用于计算当前函数何时返回并从堆栈中删除的代码来实现。在 **PAUSE** 状态中花费的时间算作“调用者”的延迟,但此时间不计入当前函数“被调用者”花费的时间。
函数计时功能具有很大的潜力。在它发挥全部潜力之前,我需要将时间转换为高精度计时器,远高于毫秒分辨率。以下是测量应用程序函数执行时间的一些可能用途:
- 确定哪些函数需要最多的处理时间。结合分析函数中发生的事情,以确定是否需要优化其性能。
- 保留以前运行的记录,以确定您的更改对性能造成的影响。
Bug:性能分析摘要未打印到日志
当我第一次创建所有工具时,我将数据写入 cout
,并且头文件包含的顺序非常不同。因此,当应用程序销毁所有 static
函数对象时,我的程序性能分析摘要被写入 stdout
。但是,在我开始编写示例应用程序并将 FnTrace.h 文件放在预编译头文件中之后,我相信我更改了一些对象的初始化顺序,从而也更改了销毁顺序。因此,当对象被销毁时,stdout
对象不再有效,并且没有地方可以接受报告的数据。这是一个我希望在不久的将来修复的错误。
演示
演示应用程序只是一个如何使用不同宏的示例。它是一个兼容 Windows Desktop 和 Window Mobile 设备的应用程序。它是 Visual Studio 2005 项目向导生成的默认应用程序,我已使用 FnTrace
框架对其进行了检测。文章顶部的示例图像是在通过 Visual Studio 2005 调试器运行此应用程序时将报告到输出窗口的数据示例。
输出
以下是演示应用程序的预期输出示例:
3068454610: int __cdecl WinMain(struct HINSTANCE__ *,struct HINSTANCE__ *,wchar_t *,int)
3068454610: hInstance = B6DE9E56
3068454610: hPrevInstance = 00000000
3068454610: lpCmdLine =
3068454610: nCmdShow = 5
3068454610: int __cdecl InitInstance(struct HINSTANCE__ *,int)
3068454610: hInstance = B6DE9E56
3068454610: nCmdShow = 5
3068454610: unsigned short __cdecl MyRegisterClass(struct HINSTANCE__ *,wchar_t *)
3068454610: hInstance = B6DE9E56
3068454610: szWindowClass = INSTRUMENT
3068454610: elapsed: 51ms
3068454610: return (50540)
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_CREATE
3068454610: hWnd = 7C079270
3068454610: message = 1
3068454610: wParam = 0
3068454610: lParam = 470153100
3068454610: elapsed: 122ms
3068454610: leave;
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_ACTIVATE
3068454610: hWnd = 7C079270
3068454610: message = 6
3068454610: wParam = 1
3068454610: lParam = 0
3068454610: elapsed: 133ms
3068454610: leave;
3068454610: elapsed: 988ms
3068454610: return (1)
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: IDM_OK
3068454610: hWnd = 7C079270
3068454610: message = 273
3068454610: wParam = 40000
3068454610: lParam = 2080871584
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_ACTIVATE
3068454610: hWnd = 7C079270
3068454610: message = 6
3068454610: wParam = 0
3068454610: lParam = 0
3068454610: elapsed: 136ms
3068454610: leave;
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_DESTROY
3068454610: hWnd = 7C079270
3068454610: message = 2
3068454610: wParam = 0
3068454610: lParam = 0
3068454610: elapsed: 114ms
3068454610: leave;
3068454610: elapsed: 0ms
3068454610: leave;
3068454610: elapsed: 0ms
3068454610: return (0)
结论
这是一项正在进行的工作,其灵感来自于对生产级应用程序代码进行检测的需求,该代码将保持不变,并且不会降低代码的可读性。此框架围绕 C++ 中可用的机制构建,并力求尽可能不显眼。
我刚刚完成了这个框架的第一次迭代,现在我将尝试使用它。如果我发现框架存在不足,我将改进本文。我欢迎任何有助于改进其设计和实现的评论或建议。
未来,我希望改进性能分析报告功能,并增加记录和报告代码覆盖路径及其他有用指标的功能。
历史
- 2008年10月8日:初始版本