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






