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

在咱們編程的時候,和字符串打交道是必不可少的,咱們對數據庫裏文本的處理,Web文本的顯示,文本數據的存儲等都須要和字符串打交道,那麼對於字符串來講,查找、拼接這些都是經常使用的操做,尤爲是以拼接使用的比較多,好比把一我的的姓名和年齡拼接在一塊兒顯示。html

在Go語言(golang)中,對於字符串的拼接處理有不少種方法,那麼那種方法纔是效率最高的呢?由於內存很貴、性能很重要,有時候不慎字符串的轉換和拷貝,就能夠把你的內存吃光,性能低下,不得不考慮。node

一個例子

對於任何功能、性能、方法的研究,沒有比例子更有說服力的啦。在這裏,咱們使用一個例子,來演示不一樣字符串的拼接方式,以及對應的性能分析。這個例子以下: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 查看性能輸出以下:bash

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倍的樣子。

www.flysnow.org/2018/10/28/…

Go語言字符串高效拼接(一) | 飛雪無情的博客

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裏不少方法耗時排在了最前面。bufferWriteString方法也比較耗時。

以上的TOP可能還不是太直觀,若是你們看火焰圖的話,就會更清晰。性能最好的是+號拼接、Join拼接,最慢的是fmt拼接,這裏的builder和buffer拼接差很少,並無發揮出其能力。

總結

從整個性能的測試和分析來看,咱們期待的builder並無發揮出來,這是否是意味着builder不實用了呢?還不如+號和Join拼接呢?咱們下一篇繼續接着分析,這裏提早透漏一些:好比:

  1. 拼接的字符串大小
  2. 拼接的字符串數量

以上這兩個很關鍵,能夠看下我上面的例子是屬於哪種。

好了,更深刻具體的,請看下一篇字符串拼接分析。

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

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