65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.94/5 (6投票s)

2008年10月8日

CPOL

7分钟阅读

viewsIcon

35599

downloadIcon

442

一个小型但功能强大的代码性能分析器,带有一个调试控制台窗口。

引言

我是佛罗里达州Full Sail大学游戏开发专业的学生。我创建了这个性能分析器,以确定哪些函数对我的游戏性能有严重影响。然后,我可以尝试优化这些函数。

这个性能分析器结合了Greeeg编写的CConsole类的一个稍微修改的版本,以及Hernán Di Pietro编写的CProfiler类的一个大量修改的版本。请访问他们的项目页面以获取更多信息。

类修改

CConsole

CConsole类唯一的修改是在Output(char* szString)函数中。我修改了它,使其接受一个字符串而不是一个char*。该函数的功能是相同的。我只做了一个简单的修改,这证明了Greeeg的类非常出色且灵活。

CProfiler

原始的CProfiler类具有非常基本的性能分析功能,它在调用ProfileStart函数时存储当前时间(精确到微秒),并在调用ProfileEnd函数时再次测量时间。函数执行所花费的时间是开始时间与结束时间之间的差值。

我借鉴了Hernán的类的基本概念,并通过添加控制台输出、分析多个函数的能力、多次分析函数并取平均执行时间等功能进行了扩展。

实现这些功能的第一步是创建一个被分析的函数结构。这个结构存储函数的名称、函数在程序中的位置、用户可能想添加的任何注释以及其他与时间测量相关的成员。然后,我让CProfiler类持有一个这些被分析函数的向量,这样我就可以将性能分析测试的结果输出到LOG文件。拥有一个被分析函数的向量也使我能够识别当前正在被分析的函数,因此用户可以在被分析的函数中包含其他被分析的函数。主要的修改是在ProfileStartProfileEnd函数的定义中进行的,在那里我创建新的分析函数,更新它们,并将它们记录到控制台或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();会添加一个换行符。

我将在使用代码部分介绍如何使用InitializeProfileStartProfileEndShutdown函数。

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文件。

在性能分析器初始化后,您就可以通过调用ProfileStartProfileEnd来开始分析函数了。例如:

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分享他们出色的程序。没有他们的知识,我不可能做到这一点。我也会因此在我的课程中丢分,因为为我们的游戏提供一个代码性能分析器是一个必需的功能。谢谢你们!

欢迎随意使用、修改和扩展这个性能分析器。它甚至可以在商业应用程序中免费使用。但请务必在您的应用程序的某个地方提及我以及其他作者。另外,请留下评论或建议,我喜欢从编程社区获得反馈。祝大家编码愉快!

© . All rights reserved.