php小編柚子為您介紹一種優化內存使用的技巧——從大對象中釋放內存。在開發過程中,我們經常會創建一些大對象,比如大數組或大型數據庫查詢結果,這些對象會占用大量內存資源。當我們使用完這些對象后,及時釋放內存是一種良好的編程習慣。本文將向您展示如何從大對象中釋放內存,以提高應用程序的性能和效率。
問題內容
我遇到了一些我不明白的事情。希望大家幫忙!
資源:
-
https://medium.com/@chaewonkong/solving-memory-leak-issues-in-go-http-clients-ba0b04574a83
https://www.golinuxcloud.com/golang-garbage-collector/
我在幾篇文章中讀到建議,在我們不再需要它們之后,我們可以通過將大切片和映射(我想這適用于所有引用類型)設置為 nil
來簡化 gc 的工作。這是我讀過的示例之一:
func ProcessResponse(resp *http.Response) error { data, err := ioutil.ReadAll(resp.Body) if err != nil { return err } // Process data here data = nil // Release memory return nil }
登錄后復制
據我了解,當函數 processresponse
完成時,data
變量將超出范圍,基本上將不再存在。然后,gc 將驗證是否沒有對 []byte
切片(data
指向的切片)的引用,并將清除內存。
將 data
設置為 nil
如何改進垃圾收集?
謝謝!
解決方法
正如其他人已經指出的那樣:在返回之前設置 data = nil
不會改變 gc 方面的任何內容。 go 編譯器將應用優化,并且 golang 的垃圾收集器在不同的階段工作。用最簡單的術語(有許多遺漏和過度簡化):設置 data = nil
,并刪除對底層切片的所有引用不會觸發不再引用的內存的原子樣式釋放。一旦切片不再被引用,它就會被標記為這樣,并且關聯的內存直到下一次掃描才會被釋放。
垃圾收集是一個難題,很大程度上是因為它不是那種具有能為所有用例產生最佳結果的最佳解決方案的問題。多年來,go 運行時已經發展了很多,重要的工作正是在運行時垃圾收集器上完成的。結果是,在極少數情況下,簡單的 somevar = nil
會產生哪怕很小的差異,更不用說明顯的差異了。
如果您正在尋找一些簡單的經驗法則類型提示,這些提示可能會影響與垃圾收集(或一般的運行時內存管理)相關的運行時開銷,我確實知道這句話似乎模糊地涵蓋了一個在你的問題中:
建議我們可以通過設置大切片和映射來簡化 gc 的工作
在分析代碼時,這可以產生顯著的結果。假設您正在讀取需要處理的大量數據,或者您必須執行某種其他類型的批處理操作并返回切片,那么人們編寫這樣的內容并不罕見:
func processstuff(input []sometypes) []resulttypes { data := []resulttypes{} for _, in := range input { data = append(data, processt(in)) } return data }
登錄后復制
通過將代碼更改為以下內容可以很容易地優化:
func processstuff(input []sometypes) []resulttypes { data := make([]resulttypes, 0, len(input)) // set cap for _, in := range input { data = append(data, processt(in)) } return data }
登錄后復制
第一個實現中發生的情況是,您使用 len
和 cap
為 0 創建一個切片。第一次調用 append
時,您超出了切片的當前容量,這將導致運行時分配內存。正如此處所解釋的,新容量的計算相當簡單,內存被分配,數據被分配復制過來:
t := make([]byte, len(s), (cap(s)+1)*2) copy(t, s)
登錄后復制
本質上,每次當要附加的切片已滿時(即 len
== cap
)調用 append
時,您將分配一個可容納: (len + 1) * 2
元素的新切片。知道在第一個示例中 data
以 len
和 cap
== 0 開頭,讓我們看看這意味著什么:
1st iteration: append creates slice with cap (0+1) *2, data is now len 1, cap 2 2nd iteration: append adds to data, now has len 2, cap 2 3rd iteration: append allocates a new slice with cap (2 + 1) *2, copies the 2 elements from data to this slice and adds the third, data is now reassigned to a slice with len 3, cap 6 4th-6th iterations: data grows to len 6, cap 6 7th iteration: same as 3rd iteration, although cap is (6 + 1) * 2, everything is copied over, data is reassigned a slice with len 7, cap 14
登錄后復制
如果切片中的數據結構較大(即許多嵌套結構、大量間接尋址等),那么這種頻繁的重新分配和復制可能會變得相當昂貴。如果您的代碼包含大量此類循環,它將開始顯示在 pprof 中(您將開始看到花費大量時間調用 gcmalloc
)。此外,如果您正在處理 15 個輸入值,您的數據切片最終將如下所示:
dataslice { len: 15 cap: 30 data underlying_array[30] }
登錄后復制
這意味著您將為 30 個值分配內存,而您只需要 15 個值,并且您將將該內存分配為 4 個逐漸增大的塊,并在每次重新分配時復制數據。
相比之下,第二個實現將在循環之前分配一個如下所示的數據片:
data { len: 0 cap: 15 data underlying_array[15] }
登錄后復制
它是一次性分配的,因此不需要重新分配和復制,并且返回的切片將占用一半的內存空間。從這個意義上說,我們首先在開始時分配更大的內存塊,以減少稍后所需的增量分配和復制調用的數量,這總體上會降低運行時成本。
如果我不知道需要多少內存怎么辦
這是一個公平的問題。這個例子并不總是適用。在這種情況下,我們知道需要多少個元素,并且可以相應地分配內存。有時,世界并不是這樣運作的。如果您不知道最終需要多少數據,那么您可以:
-
做出有根據的猜測:gc 很困難,而且與您不同的是,編譯器和 go 運行時缺乏模糊邏輯,人們必須提出現實、合理的猜測。有時它會像這樣簡單:“嗯,我從該數據源獲取數據,我們只存儲最后 n 個元素,所以最壞的情況下,我將處理 n 個元素”,有時它有點模糊,例如:您正在處理包含 sku、產品名稱和庫存數量的 csv。您知道 sku 的長度,可以假設庫存數量為 1 到 5 位數字之間的整數,產品名稱平均為 2-3 個單詞長。英文單詞的平均長度為 6 個字符,因此您可以粗略地了解 csv 行由多少字節組成:假設 sku == 10 個字符,80 個字節,產品描述 2.5 * 6 * 8 = 120 個字節,以及 ~ 4 個字節表示庫存計數 + 2 個逗號和一個換行符,平均預期行長度為 207 個字節,為了謹慎起見,我們將其稱為 200。統計輸入文件,將其大小(以字節為單位)除以 200,您應該對行數有一個可用的、稍微保守的估計。在該代碼末尾添加一些日志記錄,比較上限與估計值,然后您可以相應地調整您的預測計算。
分析您的代碼。有時,您會發現自己正在開發新功能或全新項目,而您沒有歷史數據可以依靠進行猜測。在這種情況下,您可以簡單地猜測,運行一些測試場景,或者啟動一個測試環境來提供您的代碼生產數據版本并分析代碼。當您正在主動分析一兩個切片/映射的內存使用/運行時成本時,我必須強調這是優化。僅當這是瓶頸或明顯問題時(例如,運行時內存分配阻礙了整體分析),您才應該在這方面花費時間。在絕大多數情況下,這種級別的優化將牢牢地屬于微優化的范疇。 堅持80-20原則
回顧
不,將一個簡單的切片變量設置為 nil 在 99% 的情況下不會產生太大影響。創建和附加到地圖/切片時,更可能產生影響的是通過使用 make()
+ 指定合理的 cap
值來減少無關分配。其他可以產生影響的事情是使用指針類型/接收器,盡管這是一個需要深入研究的更復雜的主題。現在,我只想說,我一直在開發一個代碼庫,該代碼庫必須對遠遠超出典型 uint64
范圍的數字進行操作,不幸的是,我們必須能夠以更精確的方式使用小數比 float64
將允許。我們通過使用像 holiman/uint256 這樣的東西解決了 uint64
問題,它使用指針接收器,并解決shopspring/decimal 的十進制問題,它使用值接收器并復制所有內容。在花費大量時間優化代碼之后,我們已經達到了使用小數時不斷復制值的性能影響已成為問題的地步。看看這些包如何實現加法等簡單操作,并嘗試找出哪個操作成本更高:
// original a, b := 1, 2 a += b // uint256 version a, b := uint256.NewUint(1), uint256.NewUint(2) a.Add(a, b) // decimal version a, b := decimal.NewFromInt(1), decimal.NewFromInt(2) a = a.Add(b)
登錄后復制
這些只是我在最近的工作中花時間優化的幾件事,但從中得到的最重要的一點是:
過早的優化是萬惡之源
當您處理更復雜的問題/代碼時,您需要花費大量精力來研究切片或映射的分配周期,因為潛在的瓶頸和優化需要付出很大的努力。您可以而且可以說應該采取措施避免過于浪費(例如,如果您知道所述切片的最終長度是多少,則設置切片上限),但您不應該浪費太多時間手工制作每一行,直到該代碼的內存占用盡可能小。成本將是:代碼更脆弱/更難以維護和閱讀,整體性能可能會惡化(說真的,你可以相信 go 運行時會做得很好),大量的血、汗和淚水,以及急劇下降在生產力方面。