Go中的一些優(yōu)化筆記,簡約而不簡單
我們這里簡單聊一下優(yōu)化本身,然后我們直接從實際的示例開始。
為什么要優(yōu)化呢?
當(dāng)你資源占有較高的話會需要很大的成本,雖然現(xiàn)在服務(wù)器資源也不是很貴,但是你還是需要針對的做一些優(yōu)化工作。
另外每個優(yōu)化應(yīng)該建立在一個benchmark的基礎(chǔ)上,需要體現(xiàn)它給我們帶來多大的收益。
下面主要從slice、string、struct、function、map、interface、channel、pointer等方面羅列了一些常見的優(yōu)化點。
數(shù)組和slice優(yōu)化篇
提前為slice分配內(nèi)存
盡量使用第三個參數(shù): make([]T, 0, len)
如果你事先不知道確切的數(shù)量并且slice是臨時的,你可以設(shè)置得大一些,只要slice在運行時不會增長。
不要忘記使用“copy”
我們盡量不要在復(fù)制時使用 append,例如,在合并兩個或多個slice時。
正確地使用迭代
如果我們有一個包含很多元素或比較大的元素的slice,我們會嘗試使用“for”或 range 單個元素。通過這種方法,可以避免不必要的復(fù)制。
學(xué)會復(fù)用slice
如果我們需要對傳入的slice進行某種操作并返回結(jié)果,我們可以直接return,但已經(jīng)修改了。這樣我們就可以避免了新的內(nèi)存分配。
不要留下未使用的slice
如果我們需要從slice中切下一小塊并僅使用它,其實主要部分也會保留下來。可以使用copy產(chǎn)生一個新的slice,而舊的對象讓GC回收。
string-字符串優(yōu)化篇
正確地進行拼接
如果拼接字符串可以在一個語句中完成,那么可以使用“+”,如果需要在循環(huán)中執(zhí)行此操作,那么可以使用string.Builder。通過“Grow”也可以預(yù)先指定builder的大小。
使用轉(zhuǎn)換優(yōu)化
由于字符串是由字節(jié)組成的,因此有時這兩種類型之間的轉(zhuǎn)換可以避免內(nèi)存分配。
使用池化技術(shù)
我們可以池化字符串,從而幫助編譯器只存儲一次相同的字符串。
避免內(nèi)存分配
我們可以使用map來替代復(fù)合鍵,我們也可以使用[]byte。盡量不要使用fmt包,因為它的所有函數(shù)都使用了反射。
struct-結(jié)構(gòu)體優(yōu)化篇
避免復(fù)制大的struct
我們理解的小struct,是指不超過 4 個字段的struct,不超過一個機器字。
標準的copy案例
轉(zhuǎn)換成interface 接收和發(fā)送到channel 替換map中的item 向slice添加元素 迭代(range)
避免通過指針來訪問struct中的字段
解引用是比較昂貴的,我們可以盡量少做,尤其是在循環(huán)中。我們也會失去使用快速寄存器的能力。
使用小型的struct
這項工作由編譯器優(yōu)化的,這意味著它的工作量很小。
通過內(nèi)存對齊來減小struct大小
我們可以對齊struct(根據(jù)字段的大小,以正確的順序排列),從而可以減小struct本身的大小。
func-函數(shù)優(yōu)化篇
使用內(nèi)聯(lián)函數(shù)或自己內(nèi)聯(lián)
我們盡量編寫一些可供編譯器內(nèi)聯(lián)的小函數(shù)——它很快,但自己從函數(shù)中嵌入代碼則更快。對于熱路徑函數(shù)尤其如此。
什么情況下不會被內(nèi)聯(lián)?
recovery 函數(shù) select 類型聲明 defer goroutine for-range
明智地選擇你的函數(shù)參數(shù)
我們盡量使用“小”參數(shù),因為它們的拷貝會被特別優(yōu)化。我們也嘗試在拷貝和GC的負載的與增長堆棧之間保持平衡。避免使用大量的參數(shù)——讓你的程序使用超快速的寄存器(寄存器的數(shù)量是有限的)
聲明一個命名好的return結(jié)果
這似乎比在函數(shù)體中聲明這些變量更高效。
保存函數(shù)中間的結(jié)果
幫助編譯器優(yōu)化你的代碼,保存中間結(jié)果,然后會有更多的選擇來優(yōu)化你的代碼。
謹慎使用“defer”
盡量不要使用 defer,或者至少 不要在循環(huán)中使用defer 。
為“hot path”提供便利
避免在這些地方分配內(nèi)存,尤其是短期對象。首先要檢查的的就是最常見的分支(if,switch)。
這里 hot path在Go源碼中[2]也出現(xiàn)多次,根據(jù)在 sync.Once 的上下文中,“hot path”是什么意思?[3]中的回答,這里翻譯為熱路徑是非常頻繁執(zhí)行的指令序列。
map優(yōu)化篇
提前分配內(nèi)存
一切都和其他地方一樣。初始化map時,指定其大小。
使用空結(jié)構(gòu)作為值
struct{}什么都不是,因此例如對信號值使用這種方法是非常有益的。
清空map
map只能增長,不能縮小。我們需要控制這一點——完全而明確地重置map。因為刪除其所有元素?zé)o濟于事。
盡量不要在鍵和值中使用指針
如果 map 不包含指針,那么 GC 就不會在它上面浪費寶貴的時間。而且要知道字符串也是指針——使用[]byte而不是字符串作為鍵。
減少更改的次數(shù)
同樣,我們不想使用指針,但我們可以使用 map 和 slice 的復(fù)合體,并將鍵存儲在 map 中,將可以不受限制地更改的值存儲在slice中。
interface優(yōu)化篇
計算內(nèi)存分配
請記住,要給一個接口賦值,你首先需要將其拷貝到某處,然后粘貼一個指針。關(guān)鍵字是拷貝。事實證明,裝箱和拆箱的成本將近似于結(jié)構(gòu)體的大小和一次分配。
選擇最佳的類型
在某些情況下,裝箱/拆箱期間不會進行內(nèi)存分配。例如,比較小的和布爾值的變量和常量、具有一個簡單字段的struct、指針(包括map、chan、func)
避免內(nèi)存分配
與其他地方一樣,我們盡量避免不必要的內(nèi)存分配。例如,將一個接口分配給一個接口,而不是裝箱兩次。
僅在需要時使用
避免在小型、頻繁調(diào)用的函數(shù)的參數(shù)和結(jié)果中使用接口。我們不需要額外的包裝和拆包。減少使用接口方法調(diào)用的頻率,哪怕只是因為它可以防止內(nèi)聯(lián)。
指針、chan、BCE(Bounds Check Elimination-邊界檢查) 優(yōu)化篇
避免不必要的解引用
尤其是在循環(huán)中,因為事實證明它太昂貴了。解引用是我們不想以犧牲自己為代價執(zhí)行的一系列必要操作。
channel使用效率是低效的
使用channel會比其他同步方法慢。另外,select 中的 case 越多,我們的程序就越慢。但是select、case + default是優(yōu)化過了的。
盡量避免不必要的邊界檢查
這也很昂貴,我們應(yīng)該盡一切可能避免它。例如,一次檢查(獲取)最大slice索引比多次檢查更正確。最好是立即嘗試獲得極端的選項。
總結(jié)
在這篇文章中,我們看到了一些相同的優(yōu)化規(guī)則。
幫助編譯器做出正確的決定。在編譯時分配內(nèi)存,使用中間結(jié)果,并盡量保持代碼的可讀性。
不要忘記使用內(nèi)置的分析和trace跟蹤工具。
最后小土也祝你在優(yōu)化的路上做到盡善盡美。
參考資料
Golang: simple optimization notes: https://medium.com/scum-gazeta/golang-simple-optimization-notes-70bc64673980
[2]hot path在Go源碼中: https://cs.opensource.google/search?q=%22hot%20path%22&ss=go%2Fgo
[3]在 sync.Once 的上下文中,“hot path”是什么意思?: https://stackoverflow.com/questions/59174176/what-does-hot-path-mean-in-the-context-of-sync-once
推薦閱讀
