Go語言字串高效拼接(二)
在上一篇關於字串拼接的文章 ofollow,noindex">Go語言字串高效拼接(一) 中,我們演示的多種字串拼接的方式,並且使用一個例子來測試了他們的效能,通過對比發現,我們覺得效能高的 Builder
並未發揮出其應該的效能,反而 +
號拼接,甚至 strings.Join
方法的效能更優越,那麼這到底是什麼原因呢?今天我們開始解開他們神祕的面紗,解開謎底。
在開始前給大家送個福利。 阿里雲雙11拼團活動,戰隊已達數百人,前三十名,已開始瓜分百萬獎金,趕緊加入 。 現在加入即可享受最低1折,1年99元的雲主機,已經可以參與瓜分百萬獎金,阿里雲雙11戰隊排30名,先邀請再購買,趕緊上車,老司機開車。
拼接函式改造
在上一篇的文章的末尾,我已經提出了2個可能性:拼接字串的數量和拼接字串的大小,現在我們就開始證明這兩種情況,為了演示方便,我們把原來的拼接函式修改一下,可以接受一個 []string
型別的引數,這樣我們就可以對切片陣列進行字串拼接,這裡直接給出所有的拼接方法的改造後實現。
func StringPlus(p []string) string{ var s string l:=len(p) for i:=0;i<l;i++{ s+=p[i] } return s } func StringFmt(p []interface{}) string{ return fmt.Sprint(p...) } func StringJoin(p []string) string{ return strings.Join(p,"") } func StringBuffer(p []string) string { var b bytes.Buffer l:=len(p) for i:=0;i<l;i++{ b.WriteString(p[i]) } return b.String() } func StringBuilder(p []string) string { var b strings.Builder l:=len(p) for i:=0;i<l;i++{ b.WriteString(p[i]) } return b.String() } 複製程式碼
以上實現中的 for
迴圈我並沒有使用 for range
,為了提高效能,具體原因請參考我的 Go語言效能優化- For Range 效能研究 。
測試用例
以上的字串拼接函式修改後,我們就可以構造不同大小的切片進行字串拼接測試了。為了模擬上次的效果,我們先用10個切片大小的字串進行拼接測試,和上一篇的測試情形差不多(也是大概10個字串拼接)。
const BLOG= "http://www.flysnow.org/" func initStrings(N int) []string{ s:=make([]string,N) for i:=0;i<N;i++{ s[i]=BLOG } return s; } func initStringi(N int) []interface{}{ s:=make([]interface{},N) for i:=0;i<N;i++{ s[i]=BLOG } return s; } 複製程式碼
這是兩個構建測試用力切片陣列的函式,可以生成N個大小的切片。第二個 initStringi
函式返回的是 []interface{}
,這是專門為 StringFmt(p []interface{})
拼接函式準備的,減少型別之間的轉換。
有了這兩個生成測試用例的函式,我們就可以構建我們的Go語言效能測試了,我們先測試10個大小的切片。
func BenchmarkStringPlus10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringPlus(p) } } func BenchmarkStringFmt10(b *testing.B) { p:= initStringi(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringFmt(p) } } func BenchmarkStringJoin10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringJoin(p) } } func BenchmarkStringBuffer10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuffer(p) } } func BenchmarkStringBuilder10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuilder(p) } } 複製程式碼
在每個效能測試函式中,我們都會呼叫 b.ResetTimer()
,這是為了避免測試用例準備時間不同,帶來的效能測試效果偏差問題,具體可以參考我的一篇文章 Go語言實戰筆記(二十二)| Go 基準測試 。
我們執行 go test -bench=. -run=NONE -benchmem
檢視結果。
BenchmarkStringPlus10-83000000593 ns/op1312 B/op9 allocs/op BenchmarkStringFmt10-85000000335 ns/op240 B/op1 allocs/op BenchmarkStringJoin10-810000000200 ns/op480 B/op2 allocs/op BenchmarkStringBuffer10-83000000452 ns/op864 B/op4 allocs/op BenchmarkStringBuilder10-810000000231 ns/op480 B/op4 allocs/op 複製程式碼
通過這次我們可以看到, +
號拼接不再具有優勢,因為 string
是不可變的,每次拼接都會生成一個新的 string
,也就是會進行一次記憶體分配,我們現在是10個大小的切片,每次操作要進行9次進行分配,佔用記憶體,所以每次操作時間都比較長,自然效能就低下。
可能有讀者記得,我們上一篇文章 Go語言字串高效拼接(一) 中, +
加號拼接的效能測試中顯示的只有2次記憶體分配,但是我們用了好多個 +
的。
func StringPlus() string{ var s string s+="暱稱"+":"+"飛雪無情"+"\n" s+="部落格"+":"+"http://www.flysnow.org/"+"\n" s+="微信公眾號"+":"+"flysnow_org" return s } 複製程式碼
再來回顧下這段程式碼,的確是有很多 +
的,但是隻有2次記憶體分配,我們可以大膽猜測,是3次 s+=
導致的,正常和我們今天測試的10個長度的切片,只有9次記憶體分配一樣。下面我們通過執行如下命令看下Go編譯器對這段程式碼的優化: go build -gcflags="-m -m" main.go
,輸出中有如下內容:
can inline StringPlus as: func() string { var s string; s = <N>; s += "暱稱:飛雪無情\n"; s += "部落格:http://www.flysnow.org/\n"; s += "微信公眾號:flysnow_org"; return s } 複製程式碼
現在一目瞭然了,其實是編譯器幫我們把字串做了優化,只剩下3個 s+=
這次,採用長度為10個切片進行測試,也很明顯測試出了 Builder
要比 Buffer
效能好很多,這個問題原因主要還是 []byte
和 string
之間的轉換, Builder
恰恰解決了這個問題。
func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf)) } 複製程式碼
很高效的解決方案。
100個字串
現在我們測試下100個字串拼接的情況,對於我們上面的程式碼,要改造非常容易,這裡直接給出測試程式碼。
func BenchmarkStringPlus100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringPlus(p) } } func BenchmarkStringFmt100(b *testing.B) { p:= initStringi(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringFmt(p) } } func BenchmarkStringJoin100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringJoin(p) } } func BenchmarkStringBuffer100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuffer(p) } } func BenchmarkStringBuilder100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuilder(p) } } 複製程式碼
現在執行效能測試,看看100個字串連線的效能怎麼樣,哪個函式最高效。
BenchmarkStringPlus100-810000019711 ns/op123168 B/op99 allocs/op BenchmarkStringFmt100-85000002615 ns/op2304 B/op1 allocs/op BenchmarkStringJoin100-81000000 1516 ns/op4608 B/op2 allocs/op BenchmarkStringBuffer100-85000002333 ns/op8112 B/op7 allocs/op BenchmarkStringBuilder100-8 1000000 1714 ns/op6752 B/op8 allocs/op 複製程式碼
+
號和我們上面分析得一樣,這次是99次記憶體分配,效能體驗越來越差,在後面的測試中,會排除掉。
fmt
和 bufrer
已經的效能也沒有提升,繼續走低。剩下比較堅挺的是 Join
和 Builder
。
1000 個字串。
測試用力和上面章節的大同小異,所以我們直接看測試結果。
BenchmarkStringPlus1000-810001611985 ns/op12136228 B/op999 allocs/op BenchmarkStringFmt1000-85000028510 ns/op24590 B/op1 allocs/op BenchmarkStringJoin1000-810000015050 ns/op49152 B/op2 allocs/op BenchmarkStringBuffer1000-810000023534 ns/op122544 B/op11 allocs/op BenchmarkStringBuilder1000-810000017996 ns/op96224 B/op16 allocs/op 複製程式碼
整體和100個字串的時候差不多,表現好的還是 Join
和 Builder
。這兩個方法的使用側重點有些不一樣, 如果有現成的陣列、切片那麼可以直接使用 Join
,但是如果沒有,並且追求靈活性拼接,還是選擇 Builder
。 Join
還是定位於有現成切片、陣列的(畢竟拼接成陣列也要時間),並且使用固定方式進行分解的,比如逗號、空格等,侷限比較大。
小結
至於10000個字串拼接我這裡就不做測試了,大家可以自己試試,看看是不是大同小異的。
從最近的這兩篇文章的分析來看,我們大概可以總結出。
-
+
連線適用於短小的、常量字串(明確的,非變數),因為編譯器會給我們優化。 -
Join
是比較統一的拼接,不太靈活 -
fmt
和buffer
基本上不推薦 -
builder
從效能和靈活性上,都是上佳的選擇。
到這裡就完了嗎?這篇文章是完了,我也該睡覺了。但是字串高效拼接還沒完,以上並不是終極效能,還可以優化,敬請期待第三篇。
本文為原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉我的原創說明」歡迎掃碼關注公眾號 flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註**……&*¥」覺得好的話,順手分享到朋友圈吧,感謝支援。
