計(jì)算機(jī)軟件水平考試:C++Q&A:性能優(yōu)化

字號(hào):

分配大量小型類對(duì)象(如:10,000小型記錄)最快和方法是什么? 當(dāng)然,MFC 序列流化對(duì)象可以完成所需的任務(wù)。但是,內(nèi)存的分配和銷毀相當(dāng)耗時(shí)。有沒有辦法對(duì)此進(jìn)行改進(jìn)?
     我無法告訴你的方法,因?yàn)槟侨Q與應(yīng)用程序的具體情況和其使用方式。性能和內(nèi)存分配是如此巨大的一個(gè)主題,有關(guān)它們已經(jīng)有很多很多書籍。沒有哪一種方案適合所有的情形。化總是需要在速度和其它資源之間進(jìn)行明智的權(quán)衡。例如,如果你愿意建立巨型索引,那么就會(huì)獲得非常快的查詢速度?;蛘咭腼@示速度快,那么就得以加載時(shí)間作為代價(jià)。因此,本文我只能就某些需要考慮的問題給你提供一個(gè)概述,以及提供一些工具和途徑以幫助你自己找到答案。
    如果你覺得程序的性能不太滿意,首先必須確定瓶頸在哪,對(duì)此要有清醒的認(rèn)識(shí)。你可以借助復(fù)雜的工具(profiler)來產(chǎn)生各種有關(guān)性能的報(bào)告,但如果只是想知道你的代碼在哪里耗時(shí),那么用一些自己編寫的簡(jiǎn)單工具即可,我寫了一個(gè)類叫 ShowTime,它可以報(bào)告代碼的某些部分執(zhí)行時(shí)要花費(fèi)多長(zhǎng)時(shí)間。為了使用它,你只需在要用時(shí)鐘的代碼塊起始處實(shí)例化一個(gè) ShowTime 堆棧對(duì)象即可:
    void CalculatePi()
    {
     ShowTime st("Calculating pi");
     // do it
    }
    這段代碼將產(chǎn)生一個(gè)象下面這樣的 TRACE 信息:
    Calculating pi: 342 msec
    ShowTime 是如何工作的呢?它為智能指針以及在代碼塊起始處和末尾處你想做某些自動(dòng)處理的地方使用常見的 C++ 構(gòu)造函數(shù)/析構(gòu)函數(shù)(ctor/dtor)模式。ShowTime 的構(gòu)造函數(shù)將時(shí)鐘時(shí)間(自從進(jìn)程啟動(dòng)后的時(shí)鐘嘀嗒數(shù))保存在某個(gè)數(shù)據(jù)成員中;析構(gòu)函數(shù)則用從最后的時(shí)鐘數(shù)中減去這個(gè)時(shí)鐘數(shù)并產(chǎn)生一條信息。由于構(gòu)造函數(shù)/析構(gòu)函數(shù)是在代碼塊的起始處/末尾處調(diào)用的,這樣便測(cè)算出總共用了多少時(shí)間。代碼如 Figure 1 所示。
    ShowTime 并不太復(fù)雜。比如,它并不考慮多線程的情況,并且也不報(bào)告在每個(gè)函數(shù)中某個(gè)工具消耗了多少時(shí)間。但是對(duì)于日常使用來說,它能給你提供應(yīng)用程序在何處耗時(shí)的很好的參考。不要忘記針對(duì) Release 版本進(jìn)行性能測(cè)試!畢竟那是你交付使用的版本。此外,Release 和 Debug 版本之間的差別可能會(huì)曲解你的結(jié)果。例如,依賴你的設(shè)置方式,debug 版本也許要進(jìn)行額外的堆棧,這樣便使應(yīng)用程序性能下降。由于在 Release 版本中沒有 TRACE 信息,所以我添加了另外一個(gè)類,PerfLog,它可以將性能統(tǒng)計(jì)定向到一個(gè)文本文件:
    // open log file
    PerfLog mylog("MyResults.log");
    現(xiàn)在 ShowTime 可以將信息寫入MyResults.log文件以及TRACE流。但是,不要忘了在交付程序之前去掉這個(gè)性能監(jiān)視。
    有了 ShowTime 在手,我可以開始回答你的問題了。我寫了一個(gè)小程序,PerfTest,這是一個(gè)典型的具備文檔的 MFC 文檔/視圖應(yīng)用,它使用三種不同的方法分配具有 20,000 條定長(zhǎng)記錄的鏈表。
    方法一是典型的 MFC 方式。鏈表的實(shí)現(xiàn)使用 MFC 的 CObList,鏈表中的每個(gè)項(xiàng)目使用單獨(dú)的表單元。每個(gè)表單元只是小小結(jié)構(gòu),此結(jié)構(gòu)保存指向上下單元的指針和對(duì)象本身。所以 CObList 的每一項(xiàng)由12個(gè)字節(jié)的開銷,但是,如果你需要幾個(gè)表指向相同項(xiàng)目的話,就必須要有幾個(gè)表單元。(例如,你想用不同的方法排序?qū)ο螅?BR>    方法二表示了第一個(gè)性能上的改進(jìn)。這里記錄本身存儲(chǔ)下一條記錄的指針,所以沒有單獨(dú)的表單元。這個(gè)方法在僅有一個(gè)鏈表的情況下才成為可能——也就是說,如果你不需要用幾個(gè)鏈表來指向以不同方式排序的相同對(duì)象。在這樣情況下,使用數(shù)組可能更有效。但即便是一個(gè)鏈表,如果要經(jīng)常修改順序,鏈表也比數(shù)組要快,因?yàn)樾薷闹羔槺仍趦?nèi)存中移動(dòng)對(duì)象要快。
    方法一和方法二都是每分配一個(gè)記錄/對(duì)象單獨(dú)調(diào)用一次 new 操作符。如果你分配20,000個(gè)對(duì)象,便調(diào)用20,000次 new 操作。方法三用單個(gè)數(shù)組一次性分配所有的 20,000 個(gè)對(duì)象:
    m_array = new CMyRecord[20000];
    記錄的鏈接則是通過設(shè)置每條記錄中指向下一條記錄的指針域?qū)崿F(xiàn)的。分配的速度快,因?yàn)橹挥幸淮魏瘮?shù)調(diào)用,但它需要一塊連續(xù)的足以容納 20,000 條記錄的內(nèi)存塊。當(dāng)然,編譯器仍然要保證對(duì)象的初始化。當(dāng)你用向量形式的 new 操作,編譯器產(chǎn)生代碼來調(diào)用每個(gè)對(duì)象的構(gòu)造函數(shù),因此有20,000次的構(gòu)造函數(shù)調(diào)用。同樣,在 delete [] 操作中會(huì)有 20,000 次的析構(gòu)函數(shù)調(diào)用。如果構(gòu)造函數(shù)/析構(gòu)函數(shù)都為空,這些調(diào)用將被優(yōu)化掉。但如果它們有實(shí)際的事可做,這個(gè)代碼將需要有限次地執(zhí)行。這時(shí),你可能要進(jìn)一步通過給該數(shù)組分配原始字節(jié)來加速性能(避免構(gòu)造函數(shù)/析構(gòu)函數(shù)調(diào)用),然后用手工編寫代碼來初始化這些對(duì)象——但現(xiàn)在這個(gè)對(duì)你已經(jīng)不成問題