建模效率是CAD软件至关重要的指标,会极大地影响应用中的设计效率,进而决定工业产品的研发周期。鉴于CAD程序的复杂性,在编写完成之前,几乎很难准确判断这段程序是否符合预期的性能要求,往往需要通过实际测试来确认。
图1是提高程序性能的一般流程。第一步是测试总体性能,如果满足要求,则结束,否则,需要分析和定位问题,然后修改相应代码,再回到第一步重新测试,直到性能满足要求。当然,也可能存在一种情况,在现有方案下无法优化到目标性能,而面临推倒重来,这不是本文的关注点。在上述流程中,如何分析和定位问题、怎样修改代码是本文要讨论的重点。
1. 分析方法
不管是哪种分析方法,基本原则是不变的,那就是查找运行热点,或者说是定位到耗时最久的部分。这个原则是非常关键的,因为优化非热点代码的性价比可能非常低。比如,有一段代码明显有问题,但只占到总时间的10%,即使将这段代码的运行效率提高100%,也仅仅能提高总效率的5%。而如果能将占总时间80%的代码提高100%,那么就能使总效率提高40%。
分析热点的方法一般可以分成4种:
- 根据经验判断。这只适用于小段程序,且非常依赖工程师的经验。
- 查看汇编代码。由于汇编语言阅读性较差,这种方式一般只适用于深度优化。
- 编写测试程序,分解运行时间。这种方法直观且灵活度高,使用的最多。
- 运用统计和分析工具。本质上与上一种方法相同,可以减少甚至不写代码,表现形式一般也更加友好。
1.1. 计时工具
首先,我们先建立一下对计算机运行速度的大概印象,以计算两点之间距离为例,计算1,000,000次,需要多少时间呢?笔者所用电脑的表现是3ms左右。因此,如果想准确查找到热点,需要有高精度的计时工具。
以Windows系统为例,提供了多个计时器,比如clock_t。但是,clock_t的计时精度只有ms级别,通过上面的例子不难看出,如果距离的计算次数小于100,000次,clock_t就无法统计到了,因此无法满足优化性能的需求。Windows提供了高精度的性能计数器,通过QueryPerformanceCounter()和QueryPerformanceFrequency()两个函数实现,QueryPerformanceCounter()用来获取计数器当前值,QueryPerformanceFrequency()用来获取计数器的频率。实际使用中,在被测试程序之前和之后,分别调用QueryPerformanceCounter(),然后获得计数差值,除以计数器频率就是被测试程序的运行时间了。QueryPerformanceCounter()的分辨率小于1 us,计时精度可以到us级别。
实际应用中需要注意的是:
- 不要直接使用计数差值dif除以频率freq,这样得到的是以s为单位的整数值,很容易就得到0(运行时间小于1s)。如果想得到us时间,应该dif * 1000 * 1000 / freq。
- 即使计时精度为us级别,但依然会存在无法统计到的项,比如在for循环中统计点距离的时间,并累加起来,得到的数值也可能是0。
1.2. 时间分解
分析热点的核心思路就是将运行时间不断分解,一般是按照类或函数为单位模块。图2是对模块A运行时间的分解,首先,将A分解成3个子模块,分别统计运行时间,D的运行时间占比很低,而B、C的运行时间占比较多,并且相差不大。因此,需要B、C这2个子模块再次进行拆解,第三层的时间统计特点是:虽然E单次运行的时间占总时间不是很高,但是被不同的2个上级模块调用了,调用的总时间让E成为第三层的热点。继续分解E,可以看出R的时间占比很大。因此,子模块R是整个模块A的热点,是应该首先考虑优化的程序模块。
上面的分解思路是理想化的,实际过程中可能比较复杂。正如下面的代码,假设模块R中是一个for循环,其中调用了2个函数,一般的分解思路是:分别统计Fun1()和Fun2(),然后将单次调用时间累加。但是,如果2个函数单次运行的时间都在1us以下呢?累加的结果都是0。一种可行的方式是:首先,注释掉Fun2(),在for循环之外加计时代码,这样就可以统计出Fun1()的运行时间;然后,取消注释Fun2(),统计出两者的总运行时间,减去Fun1()的,就是Fun2()的运行时间。实际代码可能存在更加复杂的嵌套关系,可能需要开发人员设计更好的测试代码。
for (int i = 0; i < n; i++)
{
//计时开始
Fun1();
//计时结束
//累加时间
//计时开始
Fun2();
//计时结束
//累加时间
}
其它需要注意的是:
- 运行时间的统计和分解是一个易出错的工作,需要验证分解的正确性,确保所有子模块的时间之和近似等于父模块的时间。
- 鉴于CAD程序的复杂性,不要盲目自信,主观臆断一段程序是不耗时的,可能会错失优化性能的机会。
- 若有需要,可以在Debug模式下统计运行时间,但是,最终结果还是需要在Release模式下获取和验证,两种模式下的效率差异巨大,不同模式下得到的热点也可能截然不同。
1.3. 实用工具
上面提到的时间分解方法需要编写测试代码,这是一项比较繁琐和易出错的工作。现在有一些好用的工具,可以用来测试程序性能,比如Intel推出的VTune Profiler 和 Visual Studio中内置的性能探查器。VTune Profiler可以提供采集并分析丰富的、多层次的性能数据,对运行在Intel平台上的程序非常友好。Visual Studio内置的性能探查器,可以对内存、CPU、GPU、IO等进行数据采样,针对图2中的程序,图3和图4分别是性能探查器给出的火焰图和函数运行时间排序,从这两张图中很容易发现,程序运行的热点是函数R。