在上一篇關於字串拼接的文章Go語言字串高效拼接(二) 中,我們終於為Builder
拼接正名了,果真不負眾望,尤其是拼接的字串越來越多時,其效能的優越性更加明顯。
在上一篇的結尾中,我留下懸念說其實還有優化的空間,這就是今天這篇文章,字串拼接系列的第三篇,也是字串拼接的最後一篇產生的原因,今天我們就看下如何再提升Builder
的效能。關於第一篇字串高效拼接的文章可點選
Go語言字串高效拼接(一) 檢視。
Builder 慢在哪
既然要優化Builder
拼接,那麼我們起碼知道他慢在哪,我們繼續使用我們上篇文章的測試用例,執行看下效能。
Builder10-8 5000000 258 ns/op 480 B/op 4 allocs/op
Builder100-8 1000000 2012 ns/op 6752 B/op 8 allocs/op
Builder1000-8 100000 21016 ns/op 96224 B/op 16 allocs/op
Builder10000-8 10000 195098 ns/op 1120226 B/op 25 allocs/op
複製程式碼
針對既然要優化Builder
拼接,採取了10、100、1000、10000四種不同數量的字串進行拼接測試。我們發現每次操作都有不同次數的記憶體分配,記憶體分配越多,越慢,如果引起GC,就更慢了,首先我們先優化這個,減少記憶體分配的次數。
記憶體分配優化
通過cpuprofile,檢視生成的火焰圖可以得知,runtime.growslice
函式會被頻繁的呼叫,並且時間佔比也比較長。我們檢視Builder.WriteString
的原始碼:
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
複製程式碼
可以肯定是append
方法觸發了runtime.growslice
,因為b.buf
的容量cap
不足,所以需要呼叫runtime.growslice
擴充b.buf
的容量,然後才可以追加新的元素s...
。擴容容量自然會涉及到記憶體的分配,而且追加的內容越多,內容分配的次數越多,這和我們上面效能測試的資料是一樣的。
既然問題的原因找到了,那麼我們就可以優化了,核心手段就是減少runtime.growslice
呼叫,甚至不呼叫。照著這個思路的話,我們就要提前為b.buf
分配好容量cap
。幸好Builder
為我們提供了擴充容量的方法Grow
,我們在進行WriteString
之前,先通過Grow
方法,擴充好容量即可。
現在開始改造我們的StringBuilder
函式。
//blog:www.flysnow.org
//微信公眾號:flysnow_org
func StringBuilder(p []string,cap int) string {
var b strings.Builder
l:=len(p)
b.Grow(cap)
for i:=0;i<l;i++{
b.WriteString(p[i])
}
return b.String()
}
複製程式碼
增加一個引數cap
,讓使用者告訴我們需要的容量大小。Grow
方法的實現非常簡單,就是一個通過make
函式,擴充b.buf
大小,然後再拷貝b.buf
的過程。
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
複製程式碼
那麼現在我們的效能測試用例變成如下:
func BenchmarkStringBuilder10(b *testing.B) {
p:= initStrings(10)
cap:=10*len(BLOG)
b.ResetTimer()
for i:=0;i<b.N;i++{
StringBuilder(p,cap)
}
}
func BenchmarkStringBuilder1000(b *testing.B) {
p:= initStrings(1000)
cap:=1000*len(BLOG)
b.ResetTimer()
for i:=0;i<b.N;i++{
StringBuilder(p,cap)
}
}
複製程式碼
為了說明情況和簡短程式碼,這裡只有10和1000個元素的用例,其他類似。為了把效能優化到極致,我一次性把需要的容量分配足夠。現在我們再執行效能(Benchmark)測試程式碼。
Builder10-8 10000000 123 ns/op 352 B/op 1 allocs/op
Builder100-8 2000000 898 ns/op 2688 B/op 1 allocs/op
Builder1000-8 200000 7729 ns/op 24576 B/op 1 allocs/op
Builder10000-8 20000 78678 ns/op 237568 B/op 1 allocs/op
複製程式碼
效能足足翻了1倍多,只有1次記憶體分配,每次操作佔用的記憶體也減少了一半多,降低了GC。
小結
這次優化,到了這裡,算是結束了,寫出來後,大家也會覺得不難,其背後的原理也非常情況,就是預先分配記憶體,減少append
過程中的記憶體重新分配和資料拷貝,這樣我們就可以提升很多的效能。所以對於可以預見的長度的切,都可以提前申請申請好記憶體。
字串拼接的系列,到這裡結束了,一共三個系列,希望對大家所有幫助。
本文為原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉我的原創說明」歡迎掃碼關注公眾號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註**……&*¥」覺得好的話,順手分享到朋友圈吧,感謝支援。