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