張合花,張全法,馬 冰
(鄭州大學(xué) 物理工程學(xué)院,河南 鄭州 450001)
C/C++程序可以獲得很高的運(yùn)行速度,而許多情況下對程序的運(yùn)行速度有著很高的要求,特別是需要實(shí)時(shí)處理大量信息的時(shí)候。但是C/C++程序?qū)懗龊笸€需要進(jìn)行優(yōu)化來提高速度。常用的優(yōu)化技巧包括:盡量采用自增、自減運(yùn)算和賦值縮寫,利用指針法訪問數(shù)組,合理使用內(nèi)聯(lián)函數(shù)和寄存器變量,采用位運(yùn)算代替一些乘法或除法運(yùn)算,盡可能將浮點(diǎn)數(shù)運(yùn)算轉(zhuǎn)化為整數(shù)運(yùn)算,正確運(yùn)用內(nèi)存拷貝函數(shù),等等[1-4]。
對程序優(yōu)化后通常需要測試運(yùn)行速度,以便確認(rèn)取得了優(yōu)化效果并了解優(yōu)化程度。文獻(xiàn)[5]通過編程測試程序運(yùn)行時(shí)間,這非常麻煩,并且僅適用于它小于一個(gè)時(shí)間片。實(shí)際上,VC 6.0作為許多高校教學(xué)平臺(tái)程序員慣用的開發(fā)工具,其內(nèi)部集成了一個(gè)使用非常方便的測試工具,可以用來測試程序中各個(gè)函數(shù)的執(zhí)行時(shí)間,已經(jīng)在不少方面獲得了應(yīng)用[3-4]。然而實(shí)驗(yàn)證明,在某些情況下該工具所給的測試數(shù)據(jù)非常不可靠。為此,提出了獲得具有更高可信度之函數(shù)執(zhí)行時(shí)間的方法。
利用VC 6.0新建Win32 Console Application類型的空白項(xiàng)目,然后添加頭文件MyClass.h,內(nèi)容如下(為節(jié)省篇幅,對代碼做了盡可能的簡化,并利用先注釋掉部分代碼再逐步修改的方法,將本研究所用的多個(gè)程序揉和在了一起,下同):
externint x;
classA{public:
//A(){a = x++;}
//標(biāo)記①
//~A(){a = 0;}
//標(biāo)記②
//A();
//標(biāo)記③
//~A();
//標(biāo)記④
int a;};
class B{int b;};
接著添加源文件MyClass.cpp,內(nèi)容如下:
#include "MyClass.h"
//A::A(){a = x++;}
//標(biāo)記⑤
//A::~A(){a = 0;}
//標(biāo)記⑥
最后添加源文件main.cpp,內(nèi)容如下:
#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++){
//標(biāo)記⑦
//consume();
//標(biāo)記⑧
func();}}
//標(biāo)記⑨
程序中,func()函數(shù)先利用矢量形式的new運(yùn)算符動(dòng)態(tài)創(chuàng)建變量數(shù)組,再利用矢量形式的delete運(yùn)算符動(dòng)態(tài)釋放內(nèi)存,為主要測試對象。consume()函數(shù)的功能與它相同,不過創(chuàng)建對象時(shí)所用類型不同,其作用后面說明。
利用VC 6.0提供的工具測試函數(shù)執(zhí)行時(shí)間的完整步驟是:①單擊Build彈出菜單上的Set Active Configuration菜單項(xiàng),設(shè)置程序的當(dāng)前編譯、運(yùn)行版本為Debug或Release版。②同時(shí)按下Alt和F7鍵,在彈出的對話框的Link選項(xiàng)卡上,選中Enable Profiling復(fù)選框。③單擊Build彈出菜單上的Rebuild All菜單項(xiàng),編譯、鏈接程序。④單擊Build彈出菜單上的Profile菜單項(xiàng),在彈出的對話框上確保單選按鈕Function timing處于選中狀態(tài),再點(diǎn)擊OK按鈕啟動(dòng)測試。程序退出后在Output面板上的輸出窗口即可看到各函數(shù)的執(zhí)行時(shí)間,此后步驟可以簡化,不必每次都完整進(jìn)行。
輸出結(jié)果中,F(xiàn)unc Time稱為函數(shù)的部分總執(zhí)行時(shí)間,它是多次調(diào)用所需時(shí)間的總和,但是不包括在其內(nèi)部調(diào)用其他函數(shù)所需時(shí)間。Func+Child Time稱為總執(zhí)行時(shí)間,它是多次調(diào)用所需時(shí)間的總和,且包括在其內(nèi)部調(diào)用其他函數(shù)所需時(shí)間,將其除以調(diào)用次數(shù)即為前面所說的函數(shù)執(zhí)行時(shí)間。Hit Count為函數(shù)調(diào)用次數(shù),F(xiàn)unction為對應(yīng)的函數(shù)。
上述程序稱為設(shè)計(jì)1。在其基礎(chǔ)上:將標(biāo)記①所在行前面的注釋符號刪除后的程序稱為設(shè)計(jì)2;將標(biāo)記②所在行前面的注釋符號刪除后的程序稱為設(shè)計(jì)3;將這兩行前面的注釋符號同時(shí)刪除后的程序稱為設(shè)計(jì)4。
按照C++編程思想,new運(yùn)算符內(nèi)部首先調(diào)用malloc()函數(shù)動(dòng)態(tài)分配內(nèi)存,再調(diào)用自定義類型的構(gòu)造函數(shù)初始化對象;delete運(yùn)算符內(nèi)部首先調(diào)用自定義類型的析構(gòu)函數(shù)清除對象,再調(diào)用free()函數(shù)動(dòng)態(tài)釋放內(nèi)存[6]。因此,可用來比較沒有自定義構(gòu)造函數(shù)和析構(gòu)函數(shù)、僅有前者、僅有后者、二者皆有等情況下func()函數(shù)執(zhí)行時(shí)間的差異。
實(shí)驗(yàn)所用計(jì)算機(jī)型號為Lenovo G50-70m,操作系統(tǒng)為Win10,其CPU為Intel Core i3-4030U,主頻為1.90 GHz,下同。分別在Debug和Release版下對func()函數(shù)的總執(zhí)行時(shí)間測試10次。對于Release版,優(yōu)化策略為最大速度,下同。
由于操作系統(tǒng)的多任務(wù)特性,每次運(yùn)行程序同一函數(shù)的執(zhí)行時(shí)間存在明顯差異。為此采取的措施有:利用for循環(huán)增加函數(shù)總執(zhí)行時(shí)間的有效位數(shù)并減小波動(dòng)幅度,若某次測試結(jié)果偏離平均值太多則舍棄重測,對總執(zhí)行時(shí)間測試多次求平均值,等等。得到測試數(shù)據(jù)后計(jì)算平均總執(zhí)行時(shí)間及標(biāo)準(zhǔn)偏差,結(jié)果如表1所示。VC 6.0給的時(shí)間以ms為單位,小數(shù)點(diǎn)后面有3位數(shù)字??紤]到數(shù)據(jù)的波動(dòng)性,僅給出了2~3位數(shù)字,下同。
表1 設(shè)計(jì)1~4中func()函數(shù)的總執(zhí)行時(shí)間 ms
設(shè)計(jì)2~4的平均總執(zhí)行時(shí)間均比設(shè)計(jì)1的對應(yīng)值大許多。這是因?yàn)闆]有自定義構(gòu)造函數(shù)和析構(gòu)函數(shù)時(shí),new運(yùn)算符會(huì)調(diào)用默認(rèn)構(gòu)造函數(shù),delete運(yùn)算符會(huì)調(diào)用默認(rèn)析構(gòu)函數(shù),而默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù)皆為空函數(shù),執(zhí)行速度一定比自定義構(gòu)造函數(shù)和析構(gòu)函數(shù)快許多。Release版的平均總執(zhí)行時(shí)間小于Debug版的對應(yīng)值。這是因?yàn)镈ebug版需要嵌入調(diào)試信息而Release版不需要。
Debug版下設(shè)計(jì)2和3的平均總執(zhí)行時(shí)間大約相等。這是因?yàn)樽远x構(gòu)造函數(shù)和析構(gòu)函數(shù)差別很小,二者的執(zhí)行時(shí)間差別應(yīng)該不大,而默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù)的執(zhí)行時(shí)間差別也應(yīng)該不大。Debug版下設(shè)計(jì)4的平均總執(zhí)行時(shí)間大約為設(shè)計(jì)2與3平均總執(zhí)行時(shí)間之和再減去設(shè)計(jì)1的平均總執(zhí)行時(shí)間。根據(jù)上述分析,正應(yīng)該如此。問題是,Release版下設(shè)計(jì)3的平均總執(zhí)行時(shí)間大約為設(shè)計(jì)2的20倍,設(shè)計(jì)4的平均總執(zhí)行時(shí)間大約為設(shè)計(jì)3的2倍,這不符合預(yù)期。而Release版下函數(shù)的執(zhí)行時(shí)間通常是最應(yīng)該關(guān)心的。
經(jīng)過仔細(xì)觀察發(fā)現(xiàn),Release版下對于設(shè)計(jì)2進(jìn)行測試時(shí),在VC 6.0給的結(jié)果中找不到執(zhí)行自定義構(gòu)造函數(shù)的總執(zhí)行時(shí)間,對于設(shè)計(jì)3有自定義析構(gòu)函數(shù)的總執(zhí)行時(shí)間,對于設(shè)計(jì)4二者皆有。然而很容易證明,對于設(shè)計(jì)2程序運(yùn)行時(shí)確實(shí)調(diào)用了自定義構(gòu)造函數(shù)。于是可以假設(shè):對于使用了矢量形式之new和delete運(yùn)算符的函數(shù),測試其Release版執(zhí)行時(shí)間時(shí),測試工具在僅有自定義構(gòu)造函數(shù)情況下未統(tǒng)計(jì)自定義構(gòu)造函數(shù)的執(zhí)行時(shí)間。
為了使假設(shè)更具體,在設(shè)計(jì)1的基礎(chǔ)上,將標(biāo)記③和⑤所在行前面的注釋符號同時(shí)刪除后的程序稱為設(shè)計(jì)5;將標(biāo)記④和⑥所在行前面的注釋符號同時(shí)刪除后的程序稱為設(shè)計(jì)6;將這四行前面的注釋符號同時(shí)刪除后的程序稱為設(shè)計(jì)7。設(shè)計(jì)5~7與設(shè)計(jì)2~4的區(qū)別在于,自定義構(gòu)造函數(shù)和(或)析構(gòu)函數(shù)皆由內(nèi)聯(lián)成員函數(shù)變成了非內(nèi)聯(lián)成員函數(shù)。按照同樣的方法對設(shè)計(jì)1和設(shè)計(jì)5~7中func()函數(shù)的總執(zhí)行時(shí)間進(jìn)行測試和計(jì)算,結(jié)果如表2所示。
表2 設(shè)計(jì)1和5~7中func()函數(shù)的總執(zhí)行時(shí)間 ms
此時(shí)Release版下設(shè)計(jì)5和6的平均總執(zhí)行時(shí)間大約相等,設(shè)計(jì)7的平均總執(zhí)行時(shí)間也大約為設(shè)計(jì)5與6平均總執(zhí)行時(shí)間之和再減去設(shè)計(jì)1的平均總執(zhí)行時(shí)間。因此,將前述假設(shè)具體化為:對于使用了矢量形式之new和delete運(yùn)算符的函數(shù),測試其Release版執(zhí)行時(shí)間時(shí),測試工具在僅有內(nèi)聯(lián)自定義構(gòu)造函數(shù)情況下,未統(tǒng)計(jì)自定義構(gòu)造函數(shù)的執(zhí)行時(shí)間。若果真如此,將設(shè)計(jì)2中func()函數(shù)在Release版下的總執(zhí)行時(shí)間近似取為168 ms,將比由測試工具所給數(shù)據(jù)得到的8.0 ms具有更高的可信度。
假設(shè)的正確性必須通過人工測試來驗(yàn)證。人工測試時(shí)必須設(shè)法讓函數(shù)的總執(zhí)行時(shí)間足夠長,從而使得人工測試誤差小到可以容許的程度。為此,將設(shè)計(jì)1中標(biāo)記⑦所在行的10 000改為10 000 000,此時(shí)的程序稱為設(shè)計(jì)Ⅰ。在設(shè)計(jì)Ⅰ的基礎(chǔ)上進(jìn)行上述修改,由設(shè)計(jì)2得到設(shè)計(jì)Ⅱ,以此類推,直到得到設(shè)計(jì)Ⅶ。另外注意,人工只能直接測試整個(gè)程序即main()函數(shù)的總執(zhí)行時(shí)間。
為了進(jìn)行比較,先利用測試工具按照上述方法測試main()函數(shù)的總執(zhí)行時(shí)間。不同的是僅測試1次且不再計(jì)算平均值及標(biāo)準(zhǔn)偏差。這是因?yàn)榇藭r(shí)完成一次測試所需的時(shí)間很長,例如對于設(shè)計(jì)Ⅶ測試一次耗時(shí)長達(dá)十幾分鐘,主要影響因素是測試工具本身需要時(shí)間。測試結(jié)果如表3所示。
表3 設(shè)計(jì)Ⅰ~Ⅶ中main()函數(shù)的總執(zhí)行時(shí)間 s
再利用一款蘋果手機(jī)上的秒表功能進(jìn)行人工測試,對于每個(gè)設(shè)計(jì)測試10次,然后計(jì)算平均值及標(biāo)準(zhǔn)偏差,結(jié)果在表3中同時(shí)給出。為方便操作,利用工具欄的快捷按鈕啟動(dòng)程序的同時(shí)讓秒表開始計(jì)時(shí),出現(xiàn)Press any key to continue后停止計(jì)時(shí)。另外發(fā)現(xiàn),每當(dāng)程序修改后啟動(dòng)運(yùn)行時(shí),前幾次往往明顯偏慢,需要跳過。
由于main()函數(shù)的部分執(zhí)行時(shí)間很短(參見后面實(shí)驗(yàn)結(jié)果),即使將其總執(zhí)行時(shí)間視為func()函數(shù)的總執(zhí)行時(shí)間誤差也不是很大。根據(jù)表3中的數(shù)據(jù)可知:采用測試工具時(shí)調(diào)用次數(shù)變?yōu)橐郧暗?000倍,相應(yīng)的總執(zhí)行時(shí)間也大約為以前的1000倍,這符合預(yù)期,不算很大的偏差主要是數(shù)據(jù)波動(dòng)性的影響,main()函數(shù)部分執(zhí)行時(shí)間的影響并不大;人工測試時(shí),對于Release版,無論是內(nèi)聯(lián)的還是非內(nèi)聯(lián)的,自定義構(gòu)造函數(shù)對執(zhí)行時(shí)間的貢獻(xiàn)與自定義析構(gòu)函數(shù)相差不多。這是對所作假設(shè)的支持。
對實(shí)驗(yàn)數(shù)據(jù)的進(jìn)一步分析發(fā)現(xiàn),即使所作假設(shè)成立,測試工具所給數(shù)據(jù)的可信度也值得懷疑。根據(jù)測試工具所給數(shù)據(jù):設(shè)計(jì)Ⅰ~Ⅶ中main()函數(shù)總執(zhí)行時(shí)間之比與設(shè)計(jì)1~7中func()函數(shù)總執(zhí)行時(shí)間之比基本一樣,在Debug和Release版下分別約為1252550252550,124080404080;如果認(rèn)為所作假設(shè)成立,Release版下的比值大約為1404080404080;對于相同設(shè)計(jì),Debug版的總執(zhí)行時(shí)間不超過Release版的2倍。
然而根據(jù)人工測試數(shù)據(jù):設(shè)計(jì)Ⅰ~Ⅶ中main()函數(shù)總執(zhí)行時(shí)間之比在Debug和Release版下卻大約皆為155105510;對于相同設(shè)計(jì),Debug版的總執(zhí)行時(shí)間大約為Release版的5倍。將數(shù)據(jù)的波動(dòng)性、main()函數(shù)的部分執(zhí)行時(shí)間、測試工具運(yùn)行所需的時(shí)間以及人工測試時(shí)的反應(yīng)速度等因素之影響加在一起,都不足以造成與測試工具所給數(shù)據(jù)之間如此大的差別。雖然這不否定所作假設(shè),但它確實(shí)可能是VC 6.0提供的測試工具內(nèi)部的又一個(gè)Bug,使得它在特定條件下給的結(jié)果不可信,必須進(jìn)行人工測試才能夠得到可信的結(jié)果。
人工獲取任意函數(shù)執(zhí)行時(shí)間的方法只能是間接的:程序整體完成后,測試main()函數(shù)的總執(zhí)行時(shí)間T1(亦即它的執(zhí)行時(shí)間,因沒有通過循環(huán)多次調(diào)用它);然后將對被測試函數(shù)的調(diào)用注釋掉,再次測試main()函數(shù)(其中可能包含對其他函數(shù)的必要調(diào)用)的總執(zhí)行時(shí)間T2;于是,被測試函數(shù)的總執(zhí)行時(shí)間T=T1-T2,執(zhí)行時(shí)間等于T除以調(diào)用次數(shù)。若T1比較小,減小其測試誤差的方法如前所述。若T2比較小則可以通過調(diào)用“耗時(shí)函數(shù)”來減小其測試誤差。這種耗時(shí)函數(shù)本身沒有意義,但是總執(zhí)行時(shí)間比較長,而且在測試T1和T2時(shí)不變。
在設(shè)計(jì)Ⅰ的基礎(chǔ)上,將標(biāo)記⑧所在行前面的注釋符號刪除以添加對耗時(shí)函數(shù)consume()的調(diào)用,此時(shí)的程序稱為設(shè)計(jì)ⅰ。在設(shè)計(jì)ⅰ的基礎(chǔ)上進(jìn)行上述修改,由設(shè)計(jì)2得到設(shè)計(jì)ⅱ,以此類推,直到得到設(shè)計(jì)ⅶ,再將標(biāo)記⑨所在行對func()函數(shù)的調(diào)用(注意不包括后面的兩個(gè)右花括號)注釋掉,此時(shí)的程序稱為設(shè)計(jì)ⅷ。然后按照上述方法人工測試并計(jì)算平均值及標(biāo)準(zhǔn)偏差,結(jié)果如表4所示。
表4 設(shè)計(jì)ⅰ~ⅷ中main()函數(shù)的總執(zhí)行時(shí)間 s
將表4中設(shè)計(jì)ⅰ~ⅶ的T1減去設(shè)計(jì)ⅷ的T2,得到設(shè)計(jì)ⅰ~ⅶ中func()函數(shù)的總執(zhí)行時(shí)間Tⅰ~Tⅶ,如表5所示。
表5 設(shè)計(jì)ⅰ~ⅶ中func()函數(shù)的總執(zhí)行時(shí)間 s
將表5中的結(jié)果與表3中的人工測試結(jié)果進(jìn)行比較可知,main()函數(shù)的部分執(zhí)行時(shí)間很短,如果不添加對耗時(shí)函數(shù)的調(diào)用,人工直接測試將非常困難。但它的影響還是有的,將其影響剔除后可以提高測試結(jié)果的可信度。
若按照本文提出的方法測試函數(shù)的執(zhí)行時(shí)間,其中將包含程序運(yùn)行過程中被其他任務(wù)中斷所消耗的時(shí)間。文獻(xiàn)[7]認(rèn)為不應(yīng)該包含它,但是本文認(rèn)為恰好應(yīng)該包含它,因?yàn)樵谶@樣的操作系統(tǒng)中被中斷是不可避免的,只有包含它才能反映實(shí)際情況。
本文通過實(shí)驗(yàn)證明了VC 6.0提供的函數(shù)執(zhí)行時(shí)間測試工具對于使用了矢量形式之new和delete運(yùn)算符的函數(shù)存在問題:在Release版下當(dāng)僅有內(nèi)聯(lián)自定義構(gòu)造函數(shù)時(shí),沒有統(tǒng)計(jì)自定義構(gòu)造函數(shù)的執(zhí)行時(shí)間;無論Debug版還是Release版,其所給數(shù)據(jù)的可信度都太低。此時(shí),通過人工測試調(diào)用和不調(diào)用被測試函數(shù)時(shí)main()函數(shù)的總執(zhí)行時(shí)間,再取二者之差,并采取適當(dāng)?shù)拇胧┤缍啻窝h(huán)調(diào)用、添加調(diào)用耗時(shí)函數(shù)等減小測試誤差,可以得到較高可信度的測試數(shù)據(jù)。或許還有更多的類似情況尚未發(fā)現(xiàn),相信皆可以利用這種方法解決。