在上一篇關於字符串拼接的文章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
過程中的內存重新分配和數據拷貝,這樣我們就可以提升很多的性能。所以對於可以預見的長度的切,都可以提前申請申請好內存。
字符串拼接的系列,到這裏結束了,一共三個系列,希望對大家所有幫助。