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