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

在上一篇關於字符串拼接的文章 Go語言字符串高效拼接(一) 中,咱們演示的多種字符串拼接的方式,而且使用一個例子來測試了他們的性能,經過對比發現,咱們以爲性能高的Builder並未發揮出其應該的性能,反而+號拼接,甚至strings.Join方法的性能更優越,那麼這究竟是什麼緣由呢?今天咱們開始解開他們神祕的面紗,解開謎底。html

在開始前給你們送個福利。阿里雲雙11拼團活動,戰隊已達數百人,前三十名,已開始瓜分百萬獎金,趕忙加入如今加入便可享受最低1折,1年99元的雲主機,已經能夠參與瓜分百萬獎金,阿里雲雙11戰隊排30名,先邀請再購買,趕忙上車,老司機開車。golang

拼接函數改造

在上一篇的文章的末尾,我已經提出了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個字符串拼接)。bash

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-8     3000000     593 ns/op   1312 B/op   9 allocs/op
BenchmarkStringFmt10-8      5000000     335 ns/op   240 B/op    1 allocs/op
BenchmarkStringJoin10-8     10000000    200 ns/op   480 B/op    2 allocs/op
BenchmarkStringBuffer10-8   3000000     452 ns/op   864 B/op    4 allocs/op
BenchmarkStringBuilder10-8  10000000    231 ns/op   480 B/op    4 allocs/op
複製代碼

經過此次咱們能夠看到,+號拼接再也不具備優點,由於string是不可變的,每次拼接都會生成一個新的string,也就是會進行一次內存分配,咱們如今是10個大小的切片,每次操做要進行9次進行分配,佔用內存,因此每次操做時間都比較長,天然性能就低下。優化

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

www.flysnow.org/2018/11/05/…

可能有讀者記得,咱們上一篇文章 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性能好不少,這個問題緣由主要仍是[]bytestring之間的轉換,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-8    100000  19711 ns/op     123168 B/op     99 allocs/op
BenchmarkStringFmt100-8     500000  2615 ns/op      2304 B/op       1 allocs/op
BenchmarkStringJoin100-8    1000000 1516 ns/op      4608 B/op       2 allocs/op
BenchmarkStringBuffer100-8  500000  2333 ns/op      8112 B/op       7 allocs/op
BenchmarkStringBuilder100-8 1000000 1714 ns/op      6752 B/op       8 allocs/op
複製代碼

+號和咱們上面分析得同樣,此次是99次內存分配,性能體驗愈來愈差,在後面的測試中,會排除掉。

fmtbufrer已經的性能也沒有提高,繼續走低。剩下比較堅挺的是JoinBuilder

1000 個字符串。

測試用力和上面章節的大同小異,因此咱們直接看測試結果。

BenchmarkStringPlus1000-8       1000    1611985 ns/op   12136228 B/op   999 allocs/op
BenchmarkStringFmt1000-8        50000   28510 ns/op     24590 B/op      1 allocs/op
BenchmarkStringJoin1000-8       100000  15050 ns/op     49152 B/op      2 allocs/op
BenchmarkStringBuffer1000-8     100000  23534 ns/op     122544 B/op     11 allocs/op
BenchmarkStringBuilder1000-8    100000  17996 ns/op     96224 B/op      16 allocs/op
複製代碼

總體和100個字符串的時候差很少,表現好的仍是JoinBuilder。這兩個方法的使用側重點有些不同, 若是有現成的數組、切片那麼能夠直接使用Join,可是若是沒有,而且追求靈活性拼接,仍是選擇BuilderJoin仍是定位於有現成切片、數組的(畢竟拼接成數組也要時間),而且使用固定方式進行分解的,好比逗號、空格等,侷限比較大。

小結

至於10000個字符串拼接我這裏就不作測試了,你們能夠本身試試,看看是否是大同小異的。

從最近的這兩篇文章的分析來看,咱們大概能夠總結出。

  1. + 鏈接適用於短小的、常量字符串(明確的,非變量),由於編譯器會給咱們優化。
  2. Join是比較統一的拼接,不太靈活
  3. fmtbuffer基本上不推薦
  4. builder從性能和靈活性上,都是上佳的選擇。

到這裏就完了嗎?這篇文章是完了,我也該睡覺了。可是字符串高效拼接還沒完,以上並非終極性能,還能夠優化,敬請期待第三篇。

本文爲原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉個人原創說明」歡迎掃碼關注公衆號flysnow_org或者網站www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註**……&*¥」以爲好的話,順手分享到朋友圈吧,感謝支持。

掃碼關注
相關文章
相關標籤/搜索