提高VC 6.0中函数执行时间测试可信度的研究

2019-07-17 06:45张合花张全法
中州大学学报 2019年3期
关键词:测试工具调用人工

张合花,张全法,马 冰

(郑州大学 物理工程学院,河南 郑州 450001)

C/C++程序可以获得很高的运行速度,而许多情况下对程序的运行速度有着很高的要求,特别是需要实时处理大量信息的时候。但是C/C++程序写出后往往还需要进行优化来提高速度。常用的优化技巧包括:尽量采用自增、自减运算和赋值缩写,利用指针法访问数组,合理使用内联函数和寄存器变量,采用位运算代替一些乘法或除法运算,尽可能将浮点数运算转化为整数运算,正确运用内存拷贝函数,等等[1-4]。

对程序优化后通常需要测试运行速度,以便确认取得了优化效果并了解优化程度。文献[5]通过编程测试程序运行时间,这非常麻烦,并且仅适用于它小于一个时间片。实际上,VC 6.0作为许多高校教学平台程序员惯用的开发工具,其内部集成了一个使用非常方便的测试工具,可以用来测试程序中各个函数的执行时间,已经在不少方面获得了应用[3-4]。然而实验证明,在某些情况下该工具所给的测试数据非常不可靠。为此,提出了获得具有更高可信度之函数执行时间的方法。

1 实验用程序及集成测试工具使用方法

1.1 实验用程序

利用VC 6.0新建Win32 Console Application类型的空白项目,然后添加头文件MyClass.h,内容如下(为节省篇幅,对代码做了尽可能的简化,并利用先注释掉部分代码再逐步修改的方法,将本研究所用的多个程序揉和在了一起,下同):

externint x;

classA{public:

//A(){a = x++;}

//标记①

//~A(){a = 0;}

//标记②

//A();

//标记③

//~A();

//标记④

int a;};

class B{int b;};

接着添加源文件MyClass.cpp,内容如下:

#include "MyClass.h"

//A::A(){a = x++;}

//标记⑤

//A::~A(){a = 0;}

//标记⑥

最后添加源文件main.cpp,内容如下:

#include

#include "MyClass.h"

using namespace std;

int x = 1;

voidfunc(){A *p = new A[100];delete[]p;}

void consume(){B *p = new B[100];delete[]p;}

void main(){

for(int i = 0; i < 10000; i++){

//标记⑦

//consume();

//标记⑧

func();}}

//标记⑨

程序中,func()函数先利用矢量形式的new运算符动态创建变量数组,再利用矢量形式的delete运算符动态释放内存,为主要测试对象。consume()函数的功能与它相同,不过创建对象时所用类型不同,其作用后面说明。

1.2 集成测试工具使用方法

利用VC 6.0提供的工具测试函数执行时间的完整步骤是:①单击Build弹出菜单上的Set Active Configuration菜单项,设置程序的当前编译、运行版本为Debug或Release版。②同时按下Alt和F7键,在弹出的对话框的Link选项卡上,选中Enable Profiling复选框。③单击Build弹出菜单上的Rebuild All菜单项,编译、链接程序。④单击Build弹出菜单上的Profile菜单项,在弹出的对话框上确保单选按钮Function timing处于选中状态,再点击OK按钮启动测试。程序退出后在Output面板上的输出窗口即可看到各函数的执行时间,此后步骤可以简化,不必每次都完整进行。

输出结果中,Func Time称为函数的部分总执行时间,它是多次调用所需时间的总和,但是不包括在其内部调用其他函数所需时间。Func+Child Time称为总执行时间,它是多次调用所需时间的总和,且包括在其内部调用其他函数所需时间,将其除以调用次数即为前面所说的函数执行时间。Hit Count为函数调用次数,Function为对应的函数。

2 问题的发现

2.1 发现问题所用程序

上述程序称为设计1。在其基础上:将标记①所在行前面的注释符号删除后的程序称为设计2;将标记②所在行前面的注释符号删除后的程序称为设计3;将这两行前面的注释符号同时删除后的程序称为设计4。

按照C++编程思想,new运算符内部首先调用malloc()函数动态分配内存,再调用自定义类型的构造函数初始化对象;delete运算符内部首先调用自定义类型的析构函数清除对象,再调用free()函数动态释放内存[6]。因此,可用来比较没有自定义构造函数和析构函数、仅有前者、仅有后者、二者皆有等情况下func()函数执行时间的差异。

2.2 实验条件与测试结果

实验所用计算机型号为Lenovo G50-70m,操作系统为Win10,其CPU为Intel Core i3-4030U,主频为1.90 GHz,下同。分别在Debug和Release版下对func()函数的总执行时间测试10次。对于Release版,优化策略为最大速度,下同。

由于操作系统的多任务特性,每次运行程序同一函数的执行时间存在明显差异。为此采取的措施有:利用for循环增加函数总执行时间的有效位数并减小波动幅度,若某次测试结果偏离平均值太多则舍弃重测,对总执行时间测试多次求平均值,等等。得到测试数据后计算平均总执行时间及标准偏差,结果如表1所示。VC 6.0给的时间以ms为单位,小数点后面有3位数字。考虑到数据的波动性,仅给出了2~3位数字,下同。

表1 设计1~4中func()函数的总执行时间 ms

2.3 测试结果分析

设计2~4的平均总执行时间均比设计1的对应值大许多。这是因为没有自定义构造函数和析构函数时,new运算符会调用默认构造函数,delete运算符会调用默认析构函数,而默认构造函数和析构函数皆为空函数,执行速度一定比自定义构造函数和析构函数快许多。Release版的平均总执行时间小于Debug版的对应值。这是因为Debug版需要嵌入调试信息而Release版不需要。

Debug版下设计2和3的平均总执行时间大约相等。这是因为自定义构造函数和析构函数差别很小,二者的执行时间差别应该不大,而默认构造函数和析构函数的执行时间差别也应该不大。Debug版下设计4的平均总执行时间大约为设计2与3平均总执行时间之和再减去设计1的平均总执行时间。根据上述分析,正应该如此。问题是,Release版下设计3的平均总执行时间大约为设计2的20倍,设计4的平均总执行时间大约为设计3的2倍,这不符合预期。而Release版下函数的执行时间通常是最应该关心的。

3 假设与验证

3.1 引起问题之原因的假设

经过仔细观察发现,Release版下对于设计2进行测试时,在VC 6.0给的结果中找不到执行自定义构造函数的总执行时间,对于设计3有自定义析构函数的总执行时间,对于设计4二者皆有。然而很容易证明,对于设计2程序运行时确实调用了自定义构造函数。于是可以假设:对于使用了矢量形式之new和delete运算符的函数,测试其Release版执行时间时,测试工具在仅有自定义构造函数情况下未统计自定义构造函数的执行时间。

3.2 假设的具体化

为了使假设更具体,在设计1的基础上,将标记③和⑤所在行前面的注释符号同时删除后的程序称为设计5;将标记④和⑥所在行前面的注释符号同时删除后的程序称为设计6;将这四行前面的注释符号同时删除后的程序称为设计7。设计5~7与设计2~4的区别在于,自定义构造函数和(或)析构函数皆由内联成员函数变成了非内联成员函数。按照同样的方法对设计1和设计5~7中func()函数的总执行时间进行测试和计算,结果如表2所示。

表2 设计1和5~7中func()函数的总执行时间 ms

此时Release版下设计5和6的平均总执行时间大约相等,设计7的平均总执行时间也大约为设计5与6平均总执行时间之和再减去设计1的平均总执行时间。因此,将前述假设具体化为:对于使用了矢量形式之new和delete运算符的函数,测试其Release版执行时间时,测试工具在仅有内联自定义构造函数情况下,未统计自定义构造函数的执行时间。若果真如此,将设计2中func()函数在Release版下的总执行时间近似取为168 ms,将比由测试工具所给数据得到的8.0 ms具有更高的可信度。

3.3 实验对假设的支持

假设的正确性必须通过人工测试来验证。人工测试时必须设法让函数的总执行时间足够长,从而使得人工测试误差小到可以容许的程度。为此,将设计1中标记⑦所在行的10 000改为10 000 000,此时的程序称为设计Ⅰ。在设计Ⅰ的基础上进行上述修改,由设计2得到设计Ⅱ,以此类推,直到得到设计Ⅶ。另外注意,人工只能直接测试整个程序即main()函数的总执行时间。

为了进行比较,先利用测试工具按照上述方法测试main()函数的总执行时间。不同的是仅测试1次且不再计算平均值及标准偏差。这是因为此时完成一次测试所需的时间很长,例如对于设计Ⅶ测试一次耗时长达十几分钟,主要影响因素是测试工具本身需要时间。测试结果如表3所示。

表3 设计Ⅰ~Ⅶ中main()函数的总执行时间 s

再利用一款苹果手机上的秒表功能进行人工测试,对于每个设计测试10次,然后计算平均值及标准偏差,结果在表3中同时给出。为方便操作,利用工具栏的快捷按钮启动程序的同时让秒表开始计时,出现Press any key to continue后停止计时。另外发现,每当程序修改后启动运行时,前几次往往明显偏慢,需要跳过。

由于main()函数的部分执行时间很短(参见后面实验结果),即使将其总执行时间视为func()函数的总执行时间误差也不是很大。根据表3中的数据可知:采用测试工具时调用次数变为以前的1000倍,相应的总执行时间也大约为以前的1000倍,这符合预期,不算很大的偏差主要是数据波动性的影响,main()函数部分执行时间的影响并不大;人工测试时,对于Release版,无论是内联的还是非内联的,自定义构造函数对执行时间的贡献与自定义析构函数相差不多。这是对所作假设的支持。

3.4 测试工具的可信度问题

对实验数据的进一步分析发现,即使所作假设成立,测试工具所给数据的可信度也值得怀疑。根据测试工具所给数据:设计Ⅰ~Ⅶ中main()函数总执行时间之比与设计1~7中func()函数总执行时间之比基本一样,在Debug和Release版下分别约为1252550252550,124080404080;如果认为所作假设成立,Release版下的比值大约为1404080404080;对于相同设计,Debug版的总执行时间不超过Release版的2倍。

然而根据人工测试数据:设计Ⅰ~Ⅶ中main()函数总执行时间之比在Debug和Release版下却大约皆为155105510;对于相同设计,Debug版的总执行时间大约为Release版的5倍。将数据的波动性、main()函数的部分执行时间、测试工具运行所需的时间以及人工测试时的反应速度等因素之影响加在一起,都不足以造成与测试工具所给数据之间如此大的差别。虽然这不否定所作假设,但它确实可能是VC 6.0提供的测试工具内部的又一个Bug,使得它在特定条件下给的结果不可信,必须进行人工测试才能够得到可信的结果。

4 获取高可信度的测试数据

4.1 获取任意函数执行时间的方法

人工获取任意函数执行时间的方法只能是间接的:程序整体完成后,测试main()函数的总执行时间T1(亦即它的执行时间,因没有通过循环多次调用它);然后将对被测试函数的调用注释掉,再次测试main()函数(其中可能包含对其他函数的必要调用)的总执行时间T2;于是,被测试函数的总执行时间T=T1-T2,执行时间等于T除以调用次数。若T1比较小,减小其测试误差的方法如前所述。若T2比较小则可以通过调用“耗时函数”来减小其测试误差。这种耗时函数本身没有意义,但是总执行时间比较长,而且在测试T1和T2时不变。

4.2 实验结果

在设计Ⅰ的基础上,将标记⑧所在行前面的注释符号删除以添加对耗时函数consume()的调用,此时的程序称为设计ⅰ。在设计ⅰ的基础上进行上述修改,由设计2得到设计ⅱ,以此类推,直到得到设计ⅶ,再将标记⑨所在行对func()函数的调用(注意不包括后面的两个右花括号)注释掉,此时的程序称为设计ⅷ。然后按照上述方法人工测试并计算平均值及标准偏差,结果如表4所示。

表4 设计ⅰ~ⅷ中main()函数的总执行时间 s

将表4中设计ⅰ~ⅶ的T1减去设计ⅷ的T2,得到设计ⅰ~ⅶ中func()函数的总执行时间Tⅰ~Tⅶ,如表5所示。

表5 设计ⅰ~ⅶ中func()函数的总执行时间 s

将表5中的结果与表3中的人工测试结果进行比较可知,main()函数的部分执行时间很短,如果不添加对耗时函数的调用,人工直接测试将非常困难。但它的影响还是有的,将其影响剔除后可以提高测试结果的可信度。

4.3 关于操作系统多任务特性的影响

若按照本文提出的方法测试函数的执行时间,其中将包含程序运行过程中被其他任务中断所消耗的时间。文献[7]认为不应该包含它,但是本文认为恰好应该包含它,因为在这样的操作系统中被中断是不可避免的,只有包含它才能反映实际情况。

5 结语

本文通过实验证明了VC 6.0提供的函数执行时间测试工具对于使用了矢量形式之new和delete运算符的函数存在问题:在Release版下当仅有内联自定义构造函数时,没有统计自定义构造函数的执行时间;无论Debug版还是Release版,其所给数据的可信度都太低。此时,通过人工测试调用和不调用被测试函数时main()函数的总执行时间,再取二者之差,并采取适当的措施如多次循环调用、添加调用耗时函数等减小测试误差,可以得到较高可信度的测试数据。或许还有更多的类似情况尚未发现,相信皆可以利用这种方法解决。

猜你喜欢
测试工具调用人工
人工3D脊髓能帮助瘫痪者重新行走?
人工,天然,合成
人工“美颜”
核电项目物项调用管理的应用研究
系统虚拟化环境下客户机系统调用信息捕获与分析①
基于移动平台APP测试
手车式真空断路器回路电阻测试电流线接头研究
浅谈响应时间测试分析方法
人工制冷
利用RFC技术实现SAP系统接口通信