程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 保持C/C++程序代碼的可伸縮性

保持C/C++程序代碼的可伸縮性

編輯:關於C++
   在今天,已有許多的32位應用程序感到,在32位平台上可用的虛擬內存受到了一定的限制,對程序開發者來說,即使是開始關注64位平台時,也不得不維護軟件的32位版本,這就需要一種方法,以使代碼的兩個版本都保持相當的可伸縮性。
   目前的內存剖析工具能幫助確定,當程序達到峰值內存使用量時,都發生了什麼,但是這些工具都過於關注已分配的內存塊,而不是已提交的虛擬內存地址空間,而這兩種衡量標准沒有直接的相關性,如內存洩漏、內存碎片、內存塊內的空間浪費、或過度延遲的內存單元重新分配這些因素,都會導致不必要的虛擬內存提交。運行時分析工具如IBM Rational Purify或Parasoft Inuse均可以提供內存洩漏及已用內存的描述,這些信息是非常有用的,但是,一個特殊的內存塊也許可能、也許不可能影響到虛擬內存覆蓋區,另外,甚至一個有碎片的內存堆中的一個小塊,也能直接影響到虛擬內存覆蓋區。從另一方面來說,在此范圍內的任意內存塊--甚至洩漏的塊,對虛擬內存覆蓋區來說,也不會與之有什麼關系,除非每一個此范圍內有用的內存塊能重新分配到一個更緊湊的范圍內,這就有點像Java或托管程序的垃圾回收機制,但對大多數C/C++本地應用程序來說,就絕對不可能了,因為在虛擬內存空間中,它們內存塊的位置是不確定的。
   至於本地代碼,不必要的虛擬內存使用,這個實際的問題,比未清理的內存塊這個理論上的問題,更加有實質性。未清理的內存塊可能導致虛擬內存的浪費,造成過多的系統開銷,但或者不會;這完全依賴於堆管理器是否提交了更多的虛擬內存,以支撐這種浪費。某些很小的未使用的內存塊,不會引起不必要的堆"擴展"。與其讓你來猜哪一個或多少已浪費的內存塊導致了堆擴展,倒不如學會怎樣判定出有意義的浪費是什麼。當堆中包含不再使用的內存塊時,此時通過加入對未縮減堆的檢查,就能確定出與你的程序虛擬內存要求有很大關系的、必須進行的內存塊清理。
   為找出哪一個堆中的內存塊需多留意,必須在程序中加入一些額外的代碼,以跟蹤內存堆范圍及已分配的內存塊。對額外的代碼進行條件編譯,生成一個特定的版本,也許是一個不錯的辦法。
   為達到此目的,需編寫自定義的內存分配例程,並跟蹤每一個內存塊,另有一個自定義的釋放例程,且跟蹤虛擬內存中堆的位置,請參見例1與例2的偽代碼算法。可能還需編寫自定義的訪問函數以標記出訪問過的內存塊,以便於在適當的時候釋放虛擬內存,所有這些並不需要過多的內存開銷。另一方面,如果你的程序以堆的形式使用了大量的內存,那麼將會極大地降低性能,此處的方法也不是長久之計。
   例1:
/* 輸入參數*/
ADDRESS triggerAddr
SIZE triggerSize
LIST a list of tracked heap ranges
IF (the virtual memory at triggerAddr is tracked on the list as part of a heap range)
DO
IF (triggerAddr + triggerSize >
(the tracked upper boundary of that heap range))
DO
/* 一個現有的堆范圍被擴展 */
make system call(s) to determine the base and extent of the newly committed range that contains the addresses from triggerAddr to (triggerAddr + triggerSize)
update the size of the tracked heap range to indicate its new upper limit
END
END
ELSE DO
/* 在triggerAddr中有一個新的堆范圍 */
make system call(s) to determine the base and extent of the newly committed range that contains the addresses from triggerAddr to (triggerAddr + triggerSize)
track the new committed range in the list of heap ranges
END
   例2:
/* 輸入參數 */
ADDRESS triggerAddr
SIZE triggerSize
LIST a list of tracked heap ranges
/* 局部變量 */
ADDRESS origRangeBase
SIZE origRangeSize
BOOL bFoundChange
bFoundChange = FALSE
IF (the virtual memory at triggerAddr is not tracked on the heap range list as part of a heap range)
DO
/*似乎我們已經清楚此次釋放了。*/
END
ELSE IF (an access exception occurs when memory at triggerAddr is read)
DO
bFoundChange = TRUE
END
IF (bFoundChange) DO
/*因為之前內存塊占用的空間被釋放了,所以堆占用的虛擬內存范圍就改變了。*/
make system calls to determine the bases and extents of the tracked committed heap ranges in the immediate vicinity of the decommitted range that includes the addresses from triggerAddr to (triggerAddr + triggerSize)
/*更新堆范圍跟蹤,以反映剩余提交的范圍 */
IF (any portion of the tracked heap range that contained the block at TriggerAddr is still committed)
DO
update the heap range list to track just the portion(s) of that range that remain committed
END
ELSE
DO
delete the list element that tracks the range
END
END
   跟蹤堆內存塊
   可使用自定義的內存分配函數來進行內存塊的跟蹤,而這種函數最初被稱為普通內存分配函數,舉例來說,C語言程序中一般使用malloc(),爾後,自定義的內存分配會進行以下一系列的操作:
   ·在目前已分配的內存塊列表中,跟蹤新分配的內存塊。
   ·決定是否向系統提交虛擬內存。
   ·如果虛擬內存已被提交,跟蹤包含此內存塊的堆范圍,並更新上述堆內存塊列表,以標識出從未被訪問過的內存塊。
   還需要自定義的釋放與重分配內存函數,以便通過程序中使用的內存塊的地址與大小,來更新內存塊列表。所跟蹤的堆內存塊列表應包含如下結構:
   ·內存塊的基地址。
   ·自身大小
   ·用於指示自從上次虛擬內存被提交之後,內存塊是否被訪問過的布爾值。
   當一個內存塊被釋放後,自定義的釋放代碼將會進行以下操作:
   ·如果自從上次堆擴展之後,內存塊還未被訪問過,將向程序報告。
   ·從列表中刪除跟蹤的內存塊。
   ·判定系統是否已釋放了包含此內存塊的虛擬內存。
   ·如果虛擬內存被釋放,要相應地更新,以反映剩余的堆范圍。
   當一個內存塊被重新分配時,你的自定義重新分配內存代碼必須進行以下兩種操作:首先,在釋放之後重新跟蹤內存塊,因為重新分配的內存塊可能不在原位置;其次,在分配之後也要重新跟蹤新的基地址及分配內存塊的大小。另有一個可選的方法,你可檢查是否重新分配的內存塊被移動了,如果沒有被移動,只需僅僅更新內存塊跟蹤列表,標出此內存塊的大小;如果內存塊還在同一基地址,但是增長了,此時就要檢查堆擴展,並按照前述分配內存的方法重來一遍。
   跟蹤堆自身
   堆跟蹤取決於當內存塊被分配或釋放時,虛擬內存是否分別被提交或釋放,依此可以建立一張堆內存范圍跟蹤表,以確定在程序運行期間,虛擬內存空間中堆的確切位置,跟蹤列表中應包括如下數據:
   ·跟蹤范圍的基地址
   ·自身大小
   在Windows操作系統中,這些值可通過HeapWalk()調用獲得,此處要注意的是,HeapWalk()函數調用開銷巨大,因此,只在程序需要時調用,而不是當有內存分配或釋放時都調用。另一種Windows上的方法是使用IsBadReadPointer()函數,當一個內存塊被釋放後,你可以調用這個函數快速地判定包含此內存塊的虛擬內存是否已被釋放。另一個可以跨平台的備選方法是,可試著訪問包含此內存塊的虛擬內存,並捕捉可能發生的訪問異常。此外,只有在一種情況下會考慮使用如HeapWalk()這樣開銷巨大的函數,就是需判定鄰近剩余的已提交堆范圍。
   通過一種探測堆內存提交的算法,堆內存跟蹤列表會不斷地增長,如例1中所示。要注意的是,當你的程序分配一個內存塊時,自定義的內存分配代碼也能跟蹤到這些內存塊,並使用例1中的算法來更新包含內存塊的堆列表。如果一個內存塊的分配導致了額外的虛擬內存被提交,那麼被內存塊占用的虛擬內存會在之前就釋放,或許之前就被用作別的用途。在任一情況中,必須有條件地更新堆范圍跟蹤列表: l 如果正在跟蹤已提交范圍的基地址,此時必須更新范圍的大小,以指示出新的范圍上限。
   ·否則,必須建立一張新的堆內存范圍跟蹤表。
   如果一個新的內存塊出現在一個之前未被跟蹤的堆范圍中,就滿足了以上條件,此時明智地使用前述的系統調用可高效地跟蹤堆內存范圍。
   當你的程序釋放一個內存塊時,自定義的內存釋放代碼會使用到如例2中的算法,此算法會先判定釋放的內存是否與被跟蹤的堆內存范圍有關;接下來,必須檢查已釋放內存塊占用的空間是否仍處於提交狀態,如果是,表明了即使內存塊被釋放,虛擬內存的覆蓋區也沒有發生改變,否則,你的代碼必須進行如例2結尾處的系統調用--如Windows中的HeapWalk()--以確保跟蹤的堆是最新的,且包含堆內存塊的虛擬內存已被釋放。
   如果虛擬內存已經被提交,那麼你應該檢查那些通常包含了最近被釋放的堆內存塊的內存范圍,以確認是否有此范圍內的內存被提交,而此范圍內任何被提交的內存部分都應該是在一個堆內存范圍內,特別是如果它包含了跟蹤列表上的內存塊,進行此檢查可保證進一步的准確性。接下來還有以下兩件事,如例2中所示:
   ·如果被跟蹤的堆內存范圍內任一部分被提交,必須更新你的跟蹤列表。
   ·否則,刪除列表中的元素並跟蹤新的范圍。
   如果提交的部分在中間,那麼就有可能把堆內存范圍截成好幾斷,總而言之,在你放棄跟蹤老的范圍之前,應先在全范圍內檢查一下哪一部分仍處於提交狀態。
   程序中可能會用到好幾個不同的堆,在Windows上,如果你調用HeapCreate()並把返回的句柄傳給接下來的HeapAlloc()、HeapReAlloc()、HeapFree()函數調用時,就會創建一些不同的堆;另外,如果你加載了C運行時庫DLL的多個實例,也會因為每個實例使用它們自己的堆而產生多個堆。此時,可在不同的列表中跟蹤多個堆,也可在不同的列表中跟蹤它們的內存塊。首先,這樣做的好處是,列表的查找可以變得很快,其次,當堆被"摧毀"時,你可以毫無顧慮地清除跟蹤列表,但這需要在堆創建和釋放時加入額外的代碼來完成--即分別設置和刪除對應的列表。另外,你也可管理一個堆內存塊的列表及一個堆范圍的列表,並在程序開始運行時建立它們。
   一些專業的運行時分析工具也能對分配的內存塊及堆范圍進行跟蹤,就像前面所說的自定義內存分配函數與釋放函數一樣,甚至還能通過基本的虛擬內存HASH值(ox187d690)進一步跟蹤,以便為精確的運行時錯誤檢測提供更加可靠的手段。但此處描述的方法並不足以幫助你理解何時才能找到通過程序可控制的堆內存塊,減少虛擬內存消耗的時機。
   找到適當的清理時機
   為使用跟蹤信息以精確定位那些導致虛擬內存不必要增長的堆內存塊,你還必須要記錄下內存訪問動作,並在程序讀寫堆內存時,標記出相應的內存塊跟蹤結構。如果你的堆內存塊都是通過存取函數訪問的,那麼很容易就可找到所有代碼讀寫的內存部分,並以條件編譯生成一個特定的版本。這些存取函數可查找你列表上被訪問過的內存塊,並設置布爾值作出標記。
   當虛擬內存被提交生成一個堆,你的自定義內存分配函數應取消堆中所有內存塊的標記,而在接下來,它們可能會重新被標記上,一個接一個,就像你的程序正在訪問它們。當一個取消標記的內存塊被釋放時,相應的虛擬內存也會被釋放,此時你自定義的釋放函數就會先一步釋放內存塊,以減小虛擬內存的覆蓋范圍。
   也可安排對堆內存塊作一些臨時的掃描,這也許可在每一次虛擬內存提交時進行。如果一個內存塊在經過多遍掃描後仍保持未標記狀態(也許會花很長時間),則包含此內存塊的虛擬內存范圍就必須對所跟蹤的內存塊數進行檢查。如果那個內存塊,或者一組被忽視的類似內存塊,只是單獨地與提交的虛擬內存有關系,那麼通過釋放與重新定位這些內存塊,你可能已經找到一個減少程序虛擬內存要求的好方法。
   如果你實現了此處描述的所有內存塊和堆范圍跟蹤代碼,而當這些所有的跟蹤都起作用時,那麼程序的速度將會變得很慢,主要是因為在每一次堆內存訪問時,都會進行一遍列表查找,當然,也可以通過一些快速列表查找方法如二分法查找、跳躍查找之類的來縮短查找的時間,還可使用對應每個堆的單獨列表來加速查找。如果程序使用了許多的堆內存塊,並且也找到了減少額外虛擬內存消耗的方法,以往所花費的所有精力與耐心,與此時得到的回報相比,就算不上什麼了。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved