小型 C++ 代码分析器,带调试控制台窗口






2.94/5 (6投票s)
一个小型但功能强大的代码性能分析器,带有一个调试控制台窗口。
引言
我是佛罗里达州Full Sail大学游戏开发专业的学生。我创建了这个性能分析器,以确定哪些函数对我的游戏性能有严重影响。然后,我可以尝试优化这些函数。
这个性能分析器结合了Greeeg编写的CConsole
类的一个稍微修改的版本,以及Hernán Di Pietro编写的CProfiler
类的一个大量修改的版本。请访问他们的项目页面以获取更多信息。
类修改
CConsole
对CConsole
类唯一的修改是在Output(char* szString)
函数中。我修改了它,使其接受一个字符串而不是一个char*。该函数的功能是相同的。我只做了一个简单的修改,这证明了Greeeg的类非常出色且灵活。
CProfiler
原始的CProfiler
类具有非常基本的性能分析功能,它在调用ProfileStart
函数时存储当前时间(精确到微秒),并在调用ProfileEnd
函数时再次测量时间。函数执行所花费的时间是开始时间与结束时间之间的差值。
我借鉴了Hernán的类的基本概念,并通过添加控制台输出、分析多个函数的能力、多次分析函数并取平均执行时间等功能进行了扩展。
实现这些功能的第一步是创建一个被分析的函数结构。这个结构存储函数的名称、函数在程序中的位置、用户可能想添加的任何注释以及其他与时间测量相关的成员。然后,我让CProfiler
类持有一个这些被分析函数的向量,这样我就可以将性能分析测试的结果输出到LOG文件。拥有一个被分析函数的向量也使我能够识别当前正在被分析的函数,因此用户可以在被分析的函数中包含其他被分析的函数。主要的修改是在ProfileStart
和ProfileEnd
函数的定义中进行的,在那里我创建新的分析函数,更新它们,并将它们记录到控制台或VS的OutputDebug中。
原始ProfileStart
void CProfiler::ProfileStart(LOGTYPE logtype)
{
// get and store start time
m_Retval = QueryPerformanceCounter (&m_StartCounter);
// store logging type
m_LogType = logtype;
}
更新后的ProfileStart
void CProfiler::ProfileStart(string funcName, string funcLocation, string notes,
bool profileMultipleTimes, int numTotalProfileTimesBeforeLog) // starts profiling
{
if(!profileMultipleTimes)
{
int nID = -1;
// Go through the list of functions and see if this function
// has been profiled already.
for(int i = (int)m_vProfiledFunctions.size() - 1; i >= 0; --i)
{
if(m_vProfiledFunctions[i].funcName == funcName &&
m_vProfiledFunctions[i].funcLocation == funcLocation)
nID = i;
}
// If the function hasn't been profiled, create a new one and
// add it to the vector.
if(nID == -1)
{
FUNCTION_PROFILE function;
function.funcName = funcName;
function.funcLocation = funcLocation;
function.notes = notes;
function.numTimesCalled = 1;
function.elapsedTimes.push_back(0);
m_Retval = QueryPerformanceCounter (&function.StartCounter);
m_vProfiledFunctions.push_back(function);
}
// The function has been found. Update the values and reset
// the start counter.
else
{
m_vProfiledFunctions[nID].numTimesCalled += 1;
m_vProfiledFunctions[nID].elapsedTimes.push_back(0);
m_Retval = QueryPerformanceCounter (
&m_vProfiledFunctions[nID].StartCounter);
}
}
else
{
int index = -1;
// Go through the list of functions and see if this function
// has been profiled already.
for(int i = (int)(m_vProfiledFunctions.size() - 1); i >= 0; --i)
{
if(m_vProfiledFunctions[i].funcName == funcName &&
m_vProfiledFunctions[i].funcLocation == funcLocation)
{
index = i;
break;
}
}
// The function has been found. Update the values and reset
// the start counter.
if(index != -1)
{
if(m_vProfiledFunctions[index].numTimesCalled <
m_vProfiledFunctions[index].numTotalProfileTimes + 1)
m_vProfiledFunctions[index].numTimesCalled += 1;
m_vProfiledFunctions[index].multipleCalls = true;
m_Retval = QueryPerformanceCounter (
&m_vProfiledFunctions[index].StartCounter);
}
// If the function hasn't been profiled, create a new one and
// add it to the vector.
else
{
FUNCTION_PROFILE function;
function.funcName = funcName;
function.funcLocation = funcLocation;
function.notes = notes;
function.numTimesCalled = 1;
function.multipleCalls = true;
function.numTotalProfileTimes = numTotalProfileTimesBeforeLog;
m_Retval = QueryPerformanceCounter (&function.StartCounter);
function.elapsedTimes.push_back(0);
m_vProfiledFunctions.push_back(function);
}
}
}
我知道这个函数有点混乱,可以更简单一些,但基本功能已经实现,并且能够完成工作。(再说,我才开始编程C++ 8个月…给我点宽容吧)
ProfileEnd
函数也经过了重大改造,因为它现在将结果输出到控制台或OutputDebug(VS)。如果您想确切了解正在发生的事情,请查看我示例中的CProfiler.cpp。
最后的更改是包含了Shutdown
函数。该函数基本上遍历被分析函数的向量,并将所有结果保存到一个文本LOG文件中,如果用户在Initialize
函数中的LOGTYPE
参数中指定了的话。我将在接下来的部分详细解释所有函数以及如何使用它们。
概述
CProfiler
类计算函数执行所需的时间。该类是一个单例,因此您可以在程序的任何地方访问它。性能分析器有一个调试控制台窗口来显示被分析函数的结果。它还可以将结果输出到Visual Studio的OutputDebug窗口。但是,最重要的是,它将所有被分析函数的结果保存到一个LOG文本文件中,这样您就可以在程序结束后评估结果。该性能分析器非常简单易用,但同时也非常强大。您可以分析函数内部的函数;例如,如果您想知道程序初始化需要多长时间,您可以分析Initialize
函数,但在该函数内部,您可以分析单个函数调用,以确切了解Initialize
函数内部最耗时的是什么。最后一个有趣的特性是,您可以多次分析函数(例如,在循环中),并且可以指定性能分析器要分析该函数多少次。在分析了指定次数的函数后,它将显示最后一次调用所花费的时间以及函数执行的平均时间。
CProfiler类
CProfiler
类主要由三个部分组成:控制台类、一个被分析函数结构和实际的性能分析器类。
CConsole类
CConsole
类与Greeeg编写的类几乎相同;唯一的区别是我使用字符串输出消息,而他使用char指针。如果您需要他的类的帮助,请参阅他的项目页面。
FUNCTION_PROFILE结构
struct FUNCTION_PROFILE
{
string funcName;
string funcLocation;
string notes;
UINT numTimesCalled;
UINT numTotalProfileTimes;
LARGE_INTEGER StartCounter;
bool multipleCalls;
vector<__int64> elapsedTimes;
__int64 totalElapsedTime;
FUNCTION_PROFILE()
{
funcName = "";
funcLocation = "";
notes = "";
numTimesCalled = 0;
multipleCalls = false;
numTotalProfileTimes = 1;
totalElapsedTime = 0;
ZeroMemory(&StartCounter,sizeof(StartCounter));
}
};
前几个成员很容易理解:被分析函数的名称、该函数的位置、您可能想添加的任何注释(可选)、调用次数以及您想分析函数的次数(只有当bool
multipleCalls
设置为true时才有效)。
vector<_int64> elapsedTimes
是函数时间的集合(以滴答为单位)。我们需要一个vector,因为如果一个函数被分析多次,您会想知道每次调用花费的时间。
最后,totalElapsedTime
在函数每次被调用时会增加已用时间;同样,只有当bool
multipleCalls
设置为true
时才有效。这个变量用于计算多次分析的函数执行的平均时间。
CProfiler类
CProfiler
类的声明如下:
class CProfiler
{
public:
static CProfiler* GetInstance(void);
CConsole* GetConsole() { return &console; }
void Initialize(string buildInfo, LOGTYPE logtype =
LOG_DEBUG_OUTPUT);
void Shutdown();
void ProfileStart(string funcName, string funcLocation,
string notes = "", bool profileMultipleTimes = false,
int numTotalProfileTimesBeforeLog = 1000);
void ProfileEnd (string funcName, string funcLocation);
void LogResults (int nID);
double SecsFromTicks ( __int64 ticks);
private:
CProfiler(void);
~CProfiler(void);
CProfiler(const CProfiler &ref);
CProfiler &operator=(const CProfiler &ref);
CConsole console;
string m_sBuildInfo;
LARGE_INTEGER m_QPFrequency; // ticks/sec resolution
LARGE_INTEGER m_EndCounter; // finish time
DWORD m_Retval; // return value for API functions
LOGTYPE m_LogType; // logging type
bool m_bLogMultipleNow;
// Vector of all profiled functions, necessary for the LOG file.
vector<FUNCTION_PROFILE> m_vProfiledFunctions;
};
第一次调用GetInstance()
函数时,它将创建在整个程序中使用的唯一类的实例;之后,它返回指向该类实例的指针。
GetConsole()
返回一个指向控制台类的指针,因此您可以使用它向调试控制台添加文本。例如:
CProfiler* profiler = CProfiler::GetInstance();
profiler->GetConsole()->Output("Debug Information");
profiler->GetConsole()->Output();
需要注意的是,调用profiler->GetConsole()->Output();
会添加一个换行符。
我将在使用代码部分介绍如何使用Initialize
、ProfileStart
、ProfileEnd
和Shutdown
函数。
LogResults
函数将把被分析函数的结果记录到控制台或DebugOutput(来自VS),具体取决于在Initialize
函数中指定的LOGTYPE
。
Using the Code
您需要在任何想要分析代码的地方做的第一件事是获取类的实例。由于它是一个单例类,您不能创建CProfiler
对象,所以您必须创建一个指向该类的指针。该类会自动创建自身,所以您不必担心创建它。
CProfiler* profiler = CProfiler::GetInstance();
然后,在程序的开头,您需要通过调用Initialize
函数来初始化性能分析器。
profiler->Initialize("Test Build", LOGALL);
该函数接受两个参数:一个字符串BuildInfo
和一个LOGTYPE
。
BuildInfo
是一个字符串,将在任何函数被分析之前显示在控制台窗口的顶部。它也将在LOG文件的顶部显示。
LOGTYPE
是一个带有日志选项的枚举。您可以选择以下之一:
LOG_DEBUG_OUTPUT
- 只将结果记录到VS Debug Output窗口。LOGCONSOLE
- 只将结果记录到控制台。LOGTEXTOUT
- 只将结果记录到一个文本LOG文件中。LOGALL
- 将结果记录到控制台和一个文本LOG文件。
在性能分析器初始化后,您就可以通过调用ProfileStart
和ProfileEnd
来开始分析函数了。例如:
CProfiler* profiler = CProfiler::GetInstance();
profiler->ProfileStart("TestFunction", "WinMain()",
"See how long the test function takes to execute.");
TestFunction();
profiler->ProfileEnd("TestFunction", "WinMain()");
要多次分析函数(例如,在循环中)非常相似:
while(true)
{
profiler->ProfileStart("Update", "WinMain()",
"See how long the test function takes to execute.", true, 10000);
Update();
profiler->ProfileEnd("Update", "WinMain()");
}
在函数被分析10000次后,性能分析器将计算其平均执行时间,并输出结果。
结论
天哪…在CodeProject上写文章需要很长时间…谢天谢地,终于结束了 :)
我想亲自感谢Hernán Di Pietro和Greeeg分享他们出色的程序。没有他们的知识,我不可能做到这一点。我也会因此在我的课程中丢分,因为为我们的游戏提供一个代码性能分析器是一个必需的功能。谢谢你们!
欢迎随意使用、修改和扩展这个性能分析器。它甚至可以在商业应用程序中免费使用。但请务必在您的应用程序的某个地方提及我以及其他作者。另外,请留下评论或建议,我喜欢从编程社区获得反馈。祝大家编码愉快!