[英]Why would one replace default new and delete operators?
為什么
人會替換默認的運營商new
和delete
一個自定義new
和delete
運營商?
這是繼續重載新的和刪除在非常有啟發性的C ++常見問題解答:
運算符重載。
本FAQ的后續條目是:
我應該如何編寫符合ISO C ++標准的自定義new
和delete
運算符?
注意:答案基於Scott Meyers的“更有效的C ++”課程。
(注意:這是Stack Overflow的C ++常見問題解答的一個條目。如果你想批評在這種形式下提供常見問題解答的想法,那么發布所有這些的meta上的帖子就是這樣做的地方。這個問題在C ++聊天室中受到監控,其中FAQ的想法一開始就出現了,所以你的答案很可能被那些提出想法的人閱讀。)
可能出於多種原因嘗試替換new
和delete
運算符,即:
有許多方法可能會錯誤地使用new
和delete
可能會導致可怕行為和內存泄漏的可怕動物。 每個的例子是:
在new
ed內存上使用多個delete
,而不是在使用new
分配的內存上調用delete
。
重載的運算符new
可以保留已分配地址的列表,並且重載的運算符delete
可以從列表中刪除地址,然后很容易檢測到這樣的使用錯誤。
類似地,各種編程錯誤可能導致數據溢出 (寫入超出分配塊的末尾)和欠載 (在分配塊開始之前寫入)。
重載的operator new
可以在客戶端可用的內存之前和之后過度分配塊並放置已知的字節模式(“簽名”)。 重載的運算符刪除可以檢查簽名是否仍然完好無損。 因此,通過檢查這些簽名是否不完整,可以確定在分配的塊的生命周期中某個時間發生了溢出或欠載,並且操作員刪除可以記錄該事實以及違規指針的值,從而幫助提供良好的診斷信息。
new
和delete
運算符對每個人都運行得相當好,但對於任何人來說都是最佳選擇。 這種行為源於它們僅為通用目的而設計的事實。 它們必須適應分配模式,范圍從在程序持續時間內存在的幾個塊的動態分配到大量短期對象的常量分配和釋放。 最終,運營商new
和運營商通過編譯器delete
該船采用中間路線策略。
如果您對程序的動態內存使用模式有很好的理解,通常可以發現operator new和operator delete的自定義版本比默認版本更高(性能更快,或者需要更少的內存,最高可達50%)。 當然,除非你確定自己在做什么,否則這樣做並不是一個好主意(如果你不理解所涉及的錯綜復雜,甚至不要試試這個)。
在考慮更換new
和delete
以提高效率之前,如#2中所述,您應該收集有關您的應用程序/程序如何使用動態分配的信息。 您可能想收集有關以下內容的信息:
分配塊的分配,
生命的分布,
分配順序(FIFO或LIFO或隨機),
了解一段時間內的使用模式變化,使用的最大動態內存量等。
此外,有時您可能需要收集使用信息,例如:
計算一個類的動態對象的數量,
使用動態分配等限制正在創建的對象數。
所有這些信息都可以通過替換自定義new
delete
,並在重載的new
和delete
添加診斷收集機制。
new
次優內存對齊: 許多計算機體系結構要求將特定類型的數據放置在特定種類地址的存儲器中。 例如,架構可能要求指針出現在四倍的地址處(即,四字節對齊),或者雙倍必須出現在八倍的地址處(即,八字節對齊)。 不遵守此類約束可能會導致運行時出現硬件異常。 其他體系結構更寬容,並且可能允許它在降低性能的情況下工作。帶有一些編譯器的運算符new
不保證動態分配雙精度的八字節對齊。 在這種情況下,將默認運算符new
替換為保證八字節對齊的運算符可以大大提高程序性能,並且可以是替換new
和delete
運算符的一個很好的理由。
如果您知道特定的數據結構通常一起使用,並且您希望在處理數據時最大限度地減少頁面錯誤的頻率,那么為數據結構創建一個單獨的堆是有意義的,因此它們可以在很少的情況下聚集在一起頁面盡可能。 自定義new
和delete
放置版本可以實現此類集群。
有時您希望運算符new和delete執行編譯器提供的版本不提供的操作。
例如:您可以編寫一個自定義運算符delete
,用零覆蓋釋放的內存,以提高應用程序數據的安全性。
首先,實際上有許多不同的new
和delete
運算符(實際上是一個任意數字)。
首先,有::operator new
, ::operator new[]
, ::operator delete
和::operator delete[]
。 其次,對於任何類X
,都有X::operator new
, X::operator new[]
, X::operator delete
和X::operator delete[]
。
在這些之間,重載特定於類的運算符比使用全局運算符更常見 - 特定類的內存使用遵循特定的足夠模式是相當常見的,您可以編寫運算符來提供對默認值的實質性改進。 通常,在全球范圍內准確或特別地預測內存使用情況要困難得多。
值得一提的是,雖然operator new
和operator new[]
彼此分開(同樣對於任何X::operator new
和X::operator new[]
),但兩者的要求之間沒有區別。 一個將被調用來分配一個對象,另一個用於分配一個對象數組,但每個對象仍然只需要一定量的內存,並且需要返回一個內存塊(至少)那么大的內存地址。
說到需求,可能值得回顧其他要求1 :全局運算符必須是真正的全局 - 您可能不會將其置於命名空間內或在特定的轉換單元中使其靜態。 換句話說,只有兩個級別可以發生重載:類特定的重載或全局重載。 不允許在諸如“命名空間X中的所有類”或“翻譯單元Y中的所有分配”之間的中間點。 具體類的經營者必須是static
-但你實際上並不需要聲明為靜態的-它們是靜態的你是否明確聲明他們static
或沒有。 正式地說,全局運算符多次返回內存對齊,以便它可以用於任何類型的對象。 非正式地,在一個方面有一點擺動空間:如果你得到一個小塊的請求(例如,2個字節),你只需要提供一個對齊那個大小的對象的內存,因為試圖在那里存儲更大的東西無論如何都會導致不確定的行為。
在完成了這些預備之后,讓我們回到原來的問題, 為什么你想要超載這些運算符。 首先,我應該指出,全局運營商超載的原因往往與特定類運營商超載的原因大不相同。
由於它更常見,我將首先討論特定於類的運算符。 特定於類的內存管理的主要原因是性能。 這通常有兩種形式(或兩種形式):提高速度或減少碎片。 由於內存管理器只處理特定大小的塊,因此它可以返回任何空閑塊的地址,而不是花時間檢查塊是否足夠大,如果它是塊,則將塊分成兩塊,這樣可以提高速度。 (大部分)以相同的方式減少碎片 - 例如,為N個對象預先分配足夠大的塊,可以精確地提供N個對象所需的空間; 分配一個對象的內存值將為一個對象准確分配空間,而不是單個字節。
超載全局內存管理運營商的原因有很多種。 其中許多都面向調試或檢測,例如跟蹤應用程序所需的總內存(例如,准備移植到嵌入式系統),或通過顯示分配和釋放內存之間的不匹配來調試內存問題。 另一種常見策略是在每個請求塊的邊界之前和之后分配額外的內存,並將獨特的模式寫入這些區域。 在執行結束時(也可能在其他時間),檢查這些區域以查看代碼是否已在分配的邊界之外寫入。 另一種方法是嘗試通過自動化存儲器分配或刪除的至少一些方面來改進易用性,例如使用自動垃圾收集器 。
非默認全局分配器也可用於提高性能。 典型的情況是替換一般速度很慢的默認分配器(例如,至少某些版本的MS VC ++在4.x左右會為每個分配/刪除操作調用系統HeapAlloc
和HeapFree
函數)。 我在實踐中看到的另一種可能性是在使用SSE操作時在Intel處理器上發生。 它們以128位數據運行。 雖然操作無論對齊如何都可以工作,但當數據與128位邊界對齊時,速度會提高。 一些編譯器(例如,MS VC ++再次2 )不一定強制對齊到更大的邊界,因此即使使用默認分配器的代碼可以工作,替換分配也可以為這些操作提供顯着的速度提升。
許多計算機體系結構要求將特定類型的數據放置在特定種類地址的存儲器中。 例如,架構可能要求指針出現在四倍的地址處(即,四字節對齊),或者雙倍必須出現在八倍的地址處(即,八字節對齊)。 不遵守此類約束可能會導致運行時出現硬件異常。 其他架構更寬容,並且可能允許它在降低性能的情況下工作。
澄清一下:如果架構要求 double
數據是八字節對齊的,那么就沒有什么可以優化的。 任何類型的適當大小的動態分配(例如malloc(size)
, operator new(size)
, operator new[](size)
, new char[size]
,其中size >= sizeof(double)
)保證正確對齊。 如果實現沒有做出這種保證,那就不符合要求。 在這種情況下,更改operator new
以做“正確的事”將是“修復”實現的嘗試,而不是優化。
另一方面,一些體系結構允許一種或多種數據類型的不同(或所有)對齊類型,但根據這些相同類型的對齊提供不同的性能保證。 然后,實現可以返回存儲器(再次,假設具有適當大小的請求),該存儲器被次優地對齊,並且仍然是符合的。 這就是這個例子的意思。
與使用情況統計相關:按子系統進行預算。 例如,在基於控制台的游戲中,您可能希望為3D模型幾何保留一些內存,一些用於紋理,一些用於聲音,一些用於游戲腳本等。自定義分配器可以按子系統標記每個分配並發出超出個人預算時發出警告。
帶有一些編譯器的運算符new不保證動態分配雙精度的八字節對齊。
請引用。 通常,默認的new運算符僅比malloc包裝器稍微復雜一些,按標准,它返回的內存適合於目標體系結構支持的任何數據類型。
並不是說我沒有充分的理由為自己的課程重載新的和刪除...而你在這里已經觸及了幾個合法的,但上面不是其中之一。
似乎值得重復我的答案中的列表“任何理由來重載全局新的和刪除?” 在這里 - 有關更詳細的討論,參考和其他原因,請參閱該答案(或該問題的其他答案 )。 這些原因通常適用於本地操作符重載以及默認/全局重載,以及C malloc
/ calloc
/ realloc
/ free
重載或掛鈎。
我們工作的全局new和delete運算符超載了很多原因:
- 匯集所有小的分配 - 減少開銷,減少碎片,可以提高小型重型應用程序的性能
- 制定分配與已知的生命周期-忽略所有的FreeS直到這一時期的最末端,然后自由所有的人都在一起(當然我們這樣做更多具有本地運算符重載不是全局)
- 對齊調整 - 到高速緩存行邊界等
- alloc fill - 幫助揭示未初始化變量的使用情況
- 免費填充 - 幫助揭示以前刪除的內存的使用情況
- 延遲自由 - 提高自由填充的效率,偶爾提高性能
- 哨兵或fenceposts - 幫助暴露緩沖區溢出,欠載和偶爾的狂野指針
- 重定向分配 - 考慮NUMA,特殊內存區域,甚至在內存中保持獨立系統分離(例如嵌入式腳本語言或DSL)
- 垃圾收集或清理 - 對嵌入式腳本語言也很有用
- 堆驗證 - 您可以在N個alloc / frees中遍歷堆數據結構,以確保一切正常
- 會計 ,包括泄漏跟蹤和使用快照/統計 (堆棧,分配年齡等)
我用它在特定的共享內存領域分配對象。 (這與@Russell Borogove提到的相似。)
幾年前,我為CAVE開發了軟件。 這是一個多牆VR系統。 它用一台電腦驅動每台投影機; 6是最大值(4個牆壁,地板和天花板),而3個更常見(2個牆壁和地板)。 機器通過特殊的共享內存硬件進行通信。
為了支持它,我從我的普通(非CAVE)場景類派生出來,使用一個新的“new”,它將場景信息直接放在共享內存領域。 然后我將指針傳遞給不同機器上的從屬渲染器。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.