1 內(nèi)存優(yōu)化
1.1 小對象合并成結(jié)構(gòu)體一次分配厘唾,減少內(nèi)存分配次數(shù)
做過C/C++的同學(xué)可能知道阅嘶,小對象在堆上頻繁地申請釋放载迄,會造成內(nèi)存碎片(有的叫空洞)护昧,導(dǎo)致分配大的對象時(shí)無法申請到連續(xù)的內(nèi)存空間,一般建議是采用內(nèi)存池捣炬。Go runtime底層也采用內(nèi)存池绽榛,但每個(gè)span大小為4k灭美,同時(shí)維護(hù)一個(gè)cache。cache有一個(gè)0到n的list數(shù)組铁坎,list數(shù)組的每個(gè)單元掛載的是一個(gè)鏈表,鏈表的每個(gè)節(jié)點(diǎn)就是一塊可用的內(nèi)存扩所,同一鏈表中的所有節(jié)點(diǎn)內(nèi)存塊都是大小相等的祖屏;但是不同鏈表的內(nèi)存大小是不等的买羞,也就是說list數(shù)組的一個(gè)單元存儲的是一類固定大小的內(nèi)存塊,不同單元里存儲的內(nèi)存塊大小是不等的魁兼。這就說明cache緩存的是不同類大小的內(nèi)存對象咐汞,當(dāng)然想申請的內(nèi)存大小最接近于哪類緩存內(nèi)存塊時(shí)儒鹿,就分配哪類內(nèi)存塊。當(dāng)cache不夠再向spanalloc中分配植阴。
建議:小對象合并成結(jié)構(gòu)體一次分配掠手,示意如下:
for k, v := range m {
k, v := k, v // copy for capturing by the goroutine
go func() {
// using k & v
}()
}
替換為:
for k, v := range m {
x := struct {k , v string} {k, v} // copy for capturing by the goroutine
go func() {
// using x.k & x.v
}()
}
1.2 緩存區(qū)內(nèi)容一次分配足夠大小空間狸捕,并適當(dāng)復(fù)用
在協(xié)議編解碼時(shí)灸拍,需要頻繁地操作[]byte,可以使用bytes.Buffer或其它byte緩存區(qū)對象混槐。
建議:bytes.Buffert等通過預(yù)先分配足夠大的內(nèi)存轩性,避免當(dāng)Grow時(shí)動(dòng)態(tài)申請內(nèi)存,這樣可以減少內(nèi)存分配次數(shù)。同時(shí)對于byte緩存區(qū)對象考慮適當(dāng)?shù)貜?fù)用舒岸。
1.3 slice和map采make創(chuàng)建時(shí)蛾派,預(yù)估大小指定容量
slice和map與數(shù)組不一樣,不存在固定空間大小眯杏,可以根據(jù)增加元素來動(dòng)態(tài)擴(kuò)容壳澳。
slice初始會指定一個(gè)數(shù)組巷波,當(dāng)對slice進(jìn)行append等操作時(shí),當(dāng)容量不夠時(shí)锉屈,會自動(dòng)擴(kuò)容:
如果新的大小是當(dāng)前大小2倍以上颈渊,則容量增漲為新的大兄辗稹;
否而循環(huán)以下操作:如果當(dāng)前容量小于1024乌询,按2倍增加妹田;否則每次按當(dāng)前容量1/4增漲装蓬,直到增漲的容量超過或等新大小霜浴。
map的擴(kuò)容比較復(fù)雜,每次擴(kuò)容會增加到上次容量的2倍晌纫。它的結(jié)構(gòu)體中有一個(gè)buckets和oldbuckets锹漱,用于實(shí)現(xiàn)增量擴(kuò)容:
正常情況下,直接使用buckets哥牍,oldbuckets為空毕泌;
如果正在擴(kuò)容,則oldbuckets不為空嗅辣,buckets是oldbuckets的2倍撼泛,
建議:初始化時(shí)預(yù)估大小指定容量
m := make(map[string]string, 100)
s := make([]string, 0, 100) // 注意:對于slice make時(shí),第二個(gè)參數(shù)是初始大小澡谭,第三個(gè)參數(shù)才是容量
1.4 長調(diào)用棧避免申請較多的臨時(shí)對象
goroutine的調(diào)用棧默認(rèn)大小是4K(1.7修改為2K)愿题,它采用連續(xù)棧機(jī)制,當(dāng)椡芙保空間不夠時(shí)抠忘,Go runtime會不動(dòng)擴(kuò)容:
當(dāng)棧空間不夠時(shí)崎脉,按2倍增加,原有棧的變量崆直接copy到新的棽ィ空間囚灼,變量指針指向新的空間地址;
退棧會釋放椉礼茫空間的占用灶体,GC時(shí)發(fā)現(xiàn)棧空間占用不到1/4時(shí)掐暮,則椥椋空間減少一半。
比如棧的最終大小2M路克,則極端情況下樟结,就會有10次的擴(kuò)棧操作,這會帶來性能下降精算。
建議:
控制調(diào)用棧和函數(shù)的復(fù)雜度瓢宦,不要在一個(gè)goroutine做完所有邏輯;
如查的確需要長調(diào)用棧灰羽,而考慮goroutine池化驮履,避免頻繁創(chuàng)建goroutine帶來?xiàng)鱼辙?臻g的變化。
1.5 避免頻繁創(chuàng)建臨時(shí)對象
Go在GC時(shí)會引發(fā)stop the world玫镐,即整個(gè)情況暫停倒戏。雖1.7版本已大幅優(yōu)化GC性能,1.8甚至量壞情況下GC為100us恐似。但暫停時(shí)間還是取決于臨時(shí)對象的個(gè)數(shù)杜跷,臨時(shí)對象數(shù)量越多,暫停時(shí)間可能越長蹂喻,并消耗CPU。
建議:GC優(yōu)化方式是盡可能地減少臨時(shí)對象的個(gè)數(shù):
盡量使用局部變量
所多個(gè)局部變量合并一個(gè)大的結(jié)構(gòu)體或數(shù)組捂寿,減少掃描對象的次數(shù)口四,一次回盡可能多的內(nèi)存。
2 并發(fā)優(yōu)化
2.1 高并發(fā)的任務(wù)處理使用goroutine池
goroutine雖輕量秦陋,但對于高并發(fā)的輕量任務(wù)處理蔓彩,頻繁來創(chuàng)建goroutine來執(zhí)行,執(zhí)行效率并不會太高效:
過多的goroutine創(chuàng)建驳概,會影響go runtime對goroutine調(diào)度赤嚼,以及GC消耗;
高并時(shí)若出現(xiàn)調(diào)用異常阻塞積壓顺又,大量的goroutine短時(shí)間積壓可能導(dǎo)致程序崩潰更卒。
2.2 避免高并發(fā)調(diào)用同步系統(tǒng)接口
goroutine的實(shí)現(xiàn),是通過同步來模擬異步操作稚照。在如下操作操作不會阻塞go runtime的線程調(diào)度:
網(wǎng)絡(luò)IO
鎖
channel
time.sleep
基于底層系統(tǒng)異步調(diào)用的Syscall
下面阻塞會創(chuàng)建新的調(diào)度線程:
本地IO調(diào)用
基于底層系統(tǒng)同步調(diào)用的Syscall
CGo方式調(diào)用C語言動(dòng)態(tài)庫中的調(diào)用IO或其它阻塞
網(wǎng)絡(luò)IO可以基于epoll的異步機(jī)制(或kqueue等異步機(jī)制)蹂空,但對于一些系統(tǒng)函數(shù)并沒有提供異步機(jī)制。例如常見的posix api中果录,對文件的操作就是同步操作上枕。雖有開源的fileepoll來模擬異步文件操作。但Go的Syscall還是依賴底層的操作系統(tǒng)的API弱恒。系統(tǒng)API沒有異步辨萍,Go也做不了異步化處理。
建議:把涉及到同步調(diào)用的goroutine返弹,隔離到可控的goroutine中锈玉,而不是直接高并的goroutine調(diào)用。
2.3 高并發(fā)時(shí)避免共享對象互斥
傳統(tǒng)多線程編程時(shí)义起,當(dāng)并發(fā)沖突在4~8線程時(shí)嘲玫,性能可能會出現(xiàn)拐點(diǎn)。Go中的推薦是不要通過共享內(nèi)存來通訊并扇,Go創(chuàng)建goroutine非常容易去团,當(dāng)大量goroutine共享同一互斥對象時(shí),也會在某一數(shù)量的goroutine出在拐點(diǎn)。
建議:goroutine盡量獨(dú)立土陪,無沖突地執(zhí)行昼汗;若goroutine間存在沖突,則可以采分區(qū)來控制goroutine的并發(fā)個(gè)數(shù)鬼雀,減少同一互斥對象沖突并發(fā)數(shù)顷窒。
3 其它優(yōu)化
3.1 避免使用CGO或者減少CGO調(diào)用次數(shù)
GO可以調(diào)用C庫函數(shù),但Go帶有垃圾收集器且Go的棧動(dòng)態(tài)增漲源哩,但這些無法與C無縫地對接鞋吉。Go的環(huán)境轉(zhuǎn)入C代碼執(zhí)行前,必須為C創(chuàng)建一個(gè)新的調(diào)用棧励烦,把棧變量賦值給C調(diào)用棧谓着,調(diào)用結(jié)束現(xiàn)拷貝回來。而這個(gè)調(diào)用開銷也非常大坛掠,需要維護(hù)Go與C的調(diào)用上下文赊锚,兩者調(diào)用棧的映射。相比直接的GO調(diào)用棧屉栓,單純的調(diào)用椣掀眩可能有2個(gè)甚至3個(gè)數(shù)量級以上。
建議:盡量避免使用CGO友多,無法避免時(shí)牲平,要減少跨CGO的調(diào)用次數(shù)。
3.2 減少[]byte與string之間轉(zhuǎn)換域滥,盡量采用[]byte來字符串處理
GO里面的string類型是一個(gè)不可變類型欠拾,不像c++中std:string,可以直接char*取值轉(zhuǎn)化骗绕,指向同一地址內(nèi)容藐窄;而GO中[]byte與string底層兩個(gè)不同的結(jié)構(gòu),他們之間的轉(zhuǎn)換存在實(shí)實(shí)在在的值對象拷貝酬土,所以盡量減少這種不必要的轉(zhuǎn)化
建議:存在字符串拼接等處理荆忍,盡量采用[]byte,例如:
func Prefix(b []byte) []byte {
return append([]byte("hello", b...))
}
3.3 字符串的拼接優(yōu)先考慮bytes.Buffer
由于string類型是一個(gè)不可變類型撤缴,但拼接會創(chuàng)建新的string刹枉。GO中字符串拼接常見有如下幾種方式:
string + 操作 :導(dǎo)致多次對象的分配與值拷貝
fmt.Sprintf :會動(dòng)態(tài)解析參數(shù),效率好不哪去
strings.Join :內(nèi)部是[]byte的append
bytes.Buffer :可以預(yù)先分配大小屈呕,減少對象分配與拷貝
建議:對于高性能要求微宝,優(yōu)先考慮bytes.Buffer,預(yù)先分配大小虎眨。非關(guān)鍵路徑蟋软,視簡潔使用镶摘。fmt.Sprintf可以簡化不同類型轉(zhuǎn)換與拼接。