在我們程式設計的時候,和字串打交道是必不可少的,我們對資料庫裡文字的處理,Web文字的顯示,文字資料的儲存等都需要和字串打交道,那麼對於字串來說,查詢、拼接這些都是常用的操作,尤其是以拼接使用的比較多,比如把一個人的姓名和年齡拼接在一起顯示。
在Go語言(golang)中,對於字串的拼接處理有很多種方法,那麼那種方法才是效率最高的呢?因為記憶體很貴、效能很重要,有時候不慎字串的轉換和拷貝,就可以把你的記憶體吃光,效能低下,不得不考慮。
一個例子
對於任何功能、效能、方法的研究,沒有比例子更有說服力的啦。在這裡,我們使用一個例子,來演示不同字串的拼接方式,以及對應的效能分析。這個例子如下:
暱稱:飛雪無情
部落格:http://www.flysnow.org/
微信公眾號:flysnow_org
複製程式碼
在這個例子中,通過字串拼接的方式,拼接出如上的內容,這裡特別強調,在這個例子中,換行也是字串拼接的一部分,因為我們要嚴格拼接出如上的內容。
+號拼接
這種拼接最簡單,也最容易被我們使用,因為它是不限程式語言的,比如Go語言有,Java也有,它們是+
號運算子,在執行時計算的。現在演示下這種拼接的程式碼,雖然比較簡單。
func StringPlus() string{
var s string
s+="暱稱"+":"+"飛雪無情"+"\n"
s+="部落格"+":"+"http://www.flysnow.org/"+"\n"
s+="微信公眾號"+":"+"flysnow_org"
return s
}
複製程式碼
我們可以自己寫個用例測試下,可以列印出和我們例子中一樣的內容。那麼這種最常見的字串拼接的方式效能怎麼樣的呢,我們測試下:
func BenchmarkStringPlus(b *testing.B) {
for i:=0;i<b.N;i++{
StringPlus()
}
}
複製程式碼
執行go test -bench=. -benchmem
檢視效能輸出如下:
BenchmarkStringPlus-8 20000000 108 ns/op 144 B/op 2 allocs/op
複製程式碼
每次操作需要108ns,進行2次記憶體分配,分配114位元組的記憶體。
fmt 拼接
這種拼接,藉助於fmt.Sprint
系列函式進行拼接,然後返回拼接的字串。
func StringFmt() string{
return fmt.Sprint("暱稱",":","飛雪無情","\n","部落格",":","http://www.flysnow.org/","\n","微信公眾號",":","flysnow_org")
}
複製程式碼
為了演示,程式碼沒有換行,可能在手機上影響閱讀體驗,見諒。它的效能我們也測試一下看看效果。
func BenchmarkStringFmt(b *testing.B) {
for i:=0;i<b.N;i++{
StringFmt()
}
}
複製程式碼
執行檢視測試結果:
BenchmarkStringFmt-8 5000000 385 ns/op 80 B/op 1 allocs/op
複製程式碼
雖然每次操作記憶體分配只有1次,分配80位元組也不多,但是每次操作耗時太長,效能遠沒有+
號操作快。
Join 拼接
這個是利用strings.Join
函式進行拼接,接受一個字串陣列,轉換為一個拼接好的字串。
func StringJoin() string{
s:=[]string{"暱稱",":","飛雪無情","\n","部落格",":","http://www.flysnow.org/","\n","微信公眾號",":","flysnow_org"}
return strings.Join(s,"")
}
func BenchmarkStringJoin(b *testing.B) {
for i:=0;i<b.N;i++{
StringJoin()
}
}
複製程式碼
為了方便,把效能測試的程式碼放一起了,現在看看效能測試的效果。
BenchmarkStringJoin-8 10000000 177 ns/op 160 B/op 2 allocs/op
複製程式碼
整體和+
操作相差不了太多,大概低0.5倍的樣子。
buffer 拼接
這種被用的也很多,使用的是bytes.Buffer
進行的字串拼接,它是非常靈活的一個結構體,不止可以拼接字串,還是可以byte
,rune
等,並且實現了io.Writer
介面,寫入也非常方便。
func StringBuffer() string {
var b bytes.Buffer
b.WriteString("暱稱")
b.WriteString(":")
b.WriteString("飛雪無情")
b.WriteString("\n")
b.WriteString("部落格")
b.WriteString(":")
b.WriteString("http://www.flysnow.org/")
b.WriteString("\n")
b.WriteString("微信公眾號")
b.WriteString(":")
b.WriteString("flysnow_org")
return b.String()
}
func BenchmarkStringBuffer(b *testing.B) {
for i:=0;i<b.N;i++{
StringBuffer()
}
}
複製程式碼
看看他的效能,執行輸出即可:
BenchmarkStringBuffer-8 5000000 291 ns/op 336 B/op 3 allocs/op
複製程式碼
好像並不是太好,和最差的fmt拼接差不多,和+
號,Join拼接差好遠,記憶體分配也比較多。每次操作耗時也很長。
builder 拼接
為了改進buffer拼接的效能,從go 1.10 版本開始,增加了一個builder型別,用於提升字串拼接的效能。它的使用和buffer幾乎一樣。
func StringBuilder() string {
var b strings.Builder
b.WriteString("暱稱")
b.WriteString(":")
b.WriteString("飛雪無情")
b.WriteString("\n")
b.WriteString("部落格")
b.WriteString(":")
b.WriteString("http://www.flysnow.org/")
b.WriteString("\n")
b.WriteString("微信公眾號")
b.WriteString(":")
b.WriteString("flysnow_org")
return b.String()
}
func BenchmarkStringBuilder(b *testing.B) {
for i:=0;i<b.N;i++{
StringBuilder()
}
}
複製程式碼
官方都說比buffer效能好了,我們看看效能測試的結果。
BenchmarkStringBuilder-8 10000000 170 ns/op 232 B/op 4 allocs/op
複製程式碼
的確提升了,提升了一倍,雖然每次分配的記憶體次數有點多,但是每次分配的記憶體大小比buffer要少。
效能對比
以上就是常用的字串拼接的方式,現在我們把這些測試結果,彙總到一起,對比下看看,因為Benchmark的測試,對於效能只顯示,我把測試的時間設定為3s(秒),把時間拉長便於對比測試,同時生成了cpu profile檔案,用於效能分析。
執行go test -bench=. -benchmem -benchtime=3s -cpuprofile=profile.out
得到如下測試結果:
StringPlus-8 50000000 112 ns/op 144 B/op 2 allocs/op
StringFmt-8 20000000 344 ns/op 80 B/op 1 allocs/op
StringJoin-8 30000000 171 ns/op 160 B/op 2 allocs/op
StringBuffer-8 20000000 302 ns/op 336 B/op 3 allocs/op
StringBuilder-8 30000000 171 ns/op 232 B/op 4 allocs/op
複製程式碼
我們通過go tool pprof profile.out
看下我們輸出的cpu profile資訊。這裡主要使用top命令。
Showing top 15 nodes out of 89
flat flat% sum% cum cum%
11.99s 42.55% 42.55% 11.99s 42.55% runtime.kevent
6.30s 22.36% 64.90% 6.30s 22.36% runtime.pthread_cond_wait
1.65s 5.86% 70.76% 1.65s 5.86% runtime.pthread_cond_signal
1.11s 3.94% 74.70% 1.11s 3.94% runtime.usleep
1.10s 3.90% 78.60% 1.10s 3.90% runtime.pthread_cond_timedwait_relative_np
0.58s 2.06% 80.66% 0.62s 2.20% runtime.wbBufFlush1
0.51s 1.81% 82.47% 0.51s 1.81% runtime.memmove
0.44s 1.56% 84.03% 1.81s 6.42% fmt.(*pp).printArg
0.39s 1.38% 85.42% 2.36s 8.37% fmt.(*pp).doPrint
0.36s 1.28% 86.69% 0.70s 2.48% fmt.(*buffer).WriteString (inline)
0.34s 1.21% 87.90% 0.93s 3.30% runtime.mallocgc
0.20s 0.71% 88.61% 1.20s 4.26% fmt.(*fmt).fmtS
0.18s 0.64% 89.25% 0.18s 0.64% fmt.(*fmt).truncate
0.16s 0.57% 89.82% 0.16s 0.57% runtime.memclrNoHeapPointers
0.15s 0.53% 90.35% 1.35s 4.79% fmt.(*pp).fmtString
複製程式碼
前15個,可以看到fmt拼接的方式是最差的,因為fmt裡很多方法耗時排在了最前面。buffer
的WriteString
方法也比較耗時。
以上的TOP可能還不是太直觀,如果大家看火焰圖的話,就會更清晰。效能最好的是+
號拼接、Join拼接,最慢的是fmt拼接,這裡的builder和buffer拼接差不多,並沒有發揮出其能力。
總結
從整個效能的測試和分析來看,我們期待的builder並沒有發揮出來,這是不是意味著builder不實用了呢?還不如+
號和Join拼接呢?我們下一篇繼續接著分析,這裡提前透漏一些:比如:
- 拼接的字串大小
- 拼接的字串數量
以上這兩個很關鍵,可以看下我上面的例子是屬於哪一種。
好了,更深入具體的,請看下一篇字串拼接分析。
本文為原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉我的原創說明」歡迎掃碼關注公眾號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註**……&*¥」覺得好的話,順手分享到朋友圈吧,感謝支援。