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

在上一篇關於字符串拼接的文章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過程中的內存重新分配和數據拷貝,這樣我們就可以提升很多的性能。所以對於可以預見的長度的切,都可以提前申請申請好內存。

字符串拼接的系列,到這裏結束了,一共三個系列,希望對大家所有幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章