Go語言字串高效拼接(三)

飛雪無情發表於2019-02-23

在上一篇關於字串拼接的文章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/,第一時間看後續精彩文章。「防爛人備註**……&*¥」覺得好的話,順手分享到朋友圈吧,感謝支援。

掃碼關注

相關文章