改善時間關鍵程式碼的秘訣
撰寫快速的程式碼需要瞭解應用程式的所有層面,以及它如何與系統互動。 本文建議一些較明顯的程式碼撰寫技術替代方案,以協助您確保程式碼的時間關鍵區段能令人滿意地執行。
總而言之,改善時間關鍵程式碼需要您:
知道程式的哪些部分必須快速。
知道程式碼的大小和速度。
知道新功能的成本。
知道完成工作所需的最少工作。
若要收集程式碼效能的資訊,您可以使用效能監視器 (perfmon.exe)。
本文內容:
快取遺漏和分頁錯誤
遺漏的快取命中 (在內部和外部快取) 以及分頁錯誤 (前往次要存放裝置取得程式指示和資料) 會減慢程式的效能。
CPU 快取叫用可能會花費程式 10-20 個時鐘週期。 外部快取叫用可能會花費 20-40 個時鐘週期。 分頁錯誤可能會花費一百萬個時鐘週期(假設處理器處理 5 億個指令/秒,而分頁錯誤的時間為 2 毫秒)。 因此,撰寫減少遺漏快取命中和分頁錯誤數目的程式碼,對程式執行而言最有利。
緩慢程式的一個原因是它們的分頁錯誤或快取遺漏經常多過於必要。 若要避免這個問題,請務必使用具有良好參考位置的資料結構,這表示將相關專案保持在一起。 有時候看起來很棒的資料結構因為參考位置惡劣而最後變得嚇人,而有時候相反的情況也成立。 以下提供兩個範例:
動態配置的連結清單可以減少程式效能。 當您搜尋專案,或周遊清單至結尾時,每個略過的連結可能會遺漏快取或造成分頁錯誤。 基於簡單陣列的清單實作可能較快,因為快取較佳且分頁錯誤較少。 即使您允許陣列更難成長的事實,它仍然可能更快。
使用動態配置連結清單的雜湊表可能會降低效能。 藉由擴充,使用動態配置連結清單存放內容的雜湊表效能可能實質較差。 事實上,在最後的分析中,在陣列進行簡單的線性搜尋可能實際上較快 (視情況而定)。 使用陣列型雜湊表(所謂的「封閉式雜湊處理」)是經常忽略的實作,通常具有優越的效能。
排序和搜尋
排序與許多一般作業相比,本質就很耗時。 避免不必要的減緩,最好的方式就是避免在關鍵時刻進行排序。 您可以:
延遲排序,直到非效能關鍵時間為止。
在先前的非效能關鍵時間排序資料。
只針對真正需要排序的資料部分進行排序。
有時候,您可以使用排序過的順序建置清單。 請小心,因為如果您需要以排序順序插入資料,可能需要較複雜的資料結構,而其具有不良的參考位置,導致快取遺漏和分頁錯誤。 在所有情況下都沒有任何方法可運作。 請嘗試數種方法,並測量差異。
下面是一些排序的一般祕訣:
使用庫存排序讓 Bug 減到最少。
您可以事先進行以減少排序複雜度的任何工作都是值得的。 如果一次性傳遞您的資料可簡化比較,並將排序從 O(n log n) 減少到 O(n),您幾乎肯定會領先。
請思考排序演算法的參考位置和您預期它執行的資料。
搜尋的替代選擇比排序少。 如果搜尋時間很重要,二進位搜尋或雜湊表查閱幾乎永遠是最佳選擇,但就像排序一樣,您必須將位置牢記在心。 透過小型陣列的線性搜尋可以比透過資料結構進行二進位搜尋更快,而且有許多指標會導致分頁錯誤或快取遺漏。
MFC 和類別庫
Microsoft Foundation Class (MFC) 可大幅簡化程式碼的撰寫。 在撰寫時間關鍵的程式碼時,您應該注意在部分類別中固有的額外負荷。 請檢查您的時間關鍵程式碼所使用的 MFC 程式碼,查看它是否符合您的效能要求。 下列清單顯示您應該注意的 MFC 類別和函式:
CString
MFC 會呼叫 C 執行時間程式庫,以動態方式配置CString
記憶體。 一般而言,CString
與任何其他動態配置的字串一樣有效率。 如同任何動態配置的字串,它也有動態配置和釋放的額外負荷。 堆疊上的簡單char
陣列便經常可以達到相同的目的,且更快速。 請不要使用CString
來存放常數字串。 請改用const char *
。 您對CString
物件執行的任何作業都有一些額外負荷。 使用執行時間程式庫 字串函 式的速度可能更快。CArray
提供CArray
一般陣列不需要的彈性,但您的程式可能不需要。 如果您知道陣列的特定限制,可以改用全域固定陣列。 如果使用CArray
,請使用CArray::SetSize
來建立其大小,並指定當需要重新配置時,它成長的項目個數。 否則,加入項目可能會導致您的陣列經常重新配置和複製,這樣沒有效率,且可能導致記憶體片段化。 此外,如果您將專案插入陣列中,請將後續專案移至記憶體中,CArray
而且可能需要成長陣列。 這些動作可能導致快取遺漏和分頁錯誤。 如果您瀏覽過 MFC 使用的程式碼,可能會發現您可以撰寫更專屬於您情節的程式碼,以改善效能。 例如,由於CArray
是一個範本,您可能會針對特定類型提供CArray
特製化。CList
CList
是一個多倍連結的清單,因此元素插入在清單的前端、尾端和已知位置上POSITION
很快速。 不過,以值或索引來查閱項目需要進行循序搜尋,如果清單很長便可能速度緩慢。 如果您的程式碼不需要雙重連結的清單,您可能想要使用CList
重新考慮。 使用單向連結清單可節省更新所有作業之另一個指標的額外負荷,以及該指標的記憶體。 額外的記憶體並不大,但它是快取遺漏或分頁錯誤的另一個機會。IsKindOf
此函式可以產生許多呼叫,而且可能會存取不同資料區域中的記憶體,因而導致參考位置不佳。 這對於偵錯組建很有用(例如,在 ASSERT 呼叫中),但嘗試避免在發行組建中使用。PreTranslateMessage
PreTranslateMessage
當特定視窗樹狀結構需要不同的鍵盤快速鍵,或您必須將訊息處理插入訊息幫浦時使用。PreTranslateMessage
會修改 MFC 分派訊息。 如果您覆寫PreTranslateMessage
,請只在需要的層級上進行。 例如,如果您只想要特定檢視的子系訊息,就不需要覆寫CMainFrame::PreTranslateMessage
。 請改成覆寫檢視類別的PreTranslateMessage
。請勿使用
PreTranslateMessage
來規避一般分派路徑,以處理傳送至任何視窗的任何訊息。 針對該用途,請使用 視窗程式和 MFC 訊息對應。OnIdle
閒置事件可能會發生在您不預期的時候,例如 和WM_KEYUP
事件之間WM_KEYDOWN
。 計時器可能是觸發程式碼更有效率的方式。 請勿OnIdle
強制重複呼叫,方法是產生誤訊,或一律從 的OnIdle
覆寫傳回TRUE
,這絕不會允許執行緒進入睡眠狀態。 同樣地,計時器或個別執行緒可能較適合。
共用程式庫
重複使用程式碼是合理的。 不過,如果您要使用其他人的程式碼,您應該確定您確實知道它在效能對於您而言至關重要的情況。 瞭解它的最佳方式是逐步執行原始程式碼,或使用 PView 或 效能監視器 等工具進行測量。
堆
請謹慎使用多個堆積。 以 HeapCreate
和 HeapAlloc
建立的額外堆積讓您能管理然後處置相關的配置集合。 請不要認可過多記憶體。 如果您使用多個堆積,請特別注意一開始認可的記憶體數量。
若不用多個堆積,您可以使用 Helper 函式作為程式碼與預設堆積之間的介面。 Helper 函式有助於自訂配置策略,可以改善應用程式的效能。 例如,如果您經常執行小型配置,可能會想將這些配置集中在預設堆積的一個部分。 您可以配置大區塊的記憶體,然後使用 Helper 函式從該區塊再配置出來。 然後,您就不會有多個未使用的記憶體堆積,因為配置會從預設堆積傳出。
不過在某些情況下,使用預設堆積可能減少參考位置。 請使用處理序檢視器、Spy++ 或效能監視器,來測量在堆積之間移動物件的效果。
測量您的堆積,您便可以解釋堆積上的每次配置。 使用 C 執行時間 偵錯堆積常式 來檢查點並傾印您的堆積。 您可以將輸出讀取到例如 Microsoft Excel 的試算表格式,並使用樞紐分析表來檢視結果。 請注意配置的總數、大小和分佈。 將這些結果與工作集的大小進行比較。 另外也請查看相關大小之物件的群集。
您也可以使用效能計數器來監控記憶體使用量。
執行緒
對於背景工作,有效的事件閒置處理可能比使用執行緒更快。 在單一執行緒程式中比較容易瞭解參考位置。
有一項不錯的準則是,唯有在您封鎖的作業系統通知是背景工作的根源時才使用執行緒。 執行緒是這類情況下最好的解決方案,因為封鎖事件的主要執行緒是不切實際的。
執行緒也會造成通訊問題。 您必須使用訊息清單或是藉由配置並使用共用記憶體,來管理執行緒之間的通訊連結。 管理通訊連結通常需要同步處理,以避免競爭情況和死結問題。 這種複雜性很有可能輕易地便演變成 Bug 和效能問題。
小型工作集
較小的工作集表示參考位置較佳、較少分頁錯誤,且會有較多的快取命中。 處理序工作集是作業系統直接提供來測量參考位置的最接近度量。
若要設定工作集的上限和下限,請使用
SetProcessWorkingSetSize
。若要取得工作集的上限和下限,請使用
GetProcessWorkingSetSize
。若要檢視工作集的大小,請使用 Spy++。
另請參閱
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應