go字符串拼接方式及性能比拼小結(jié)
在golang中字符串的拼接方式有多種,本文將會(huì)介紹比較常用的幾種方式,并且對(duì)各種方式進(jìn)行壓測,以此來得到在不同場景下更適合使用的方案。
1、go字符串的幾種拼接方式
比如對(duì)于三個(gè)字符串,s1、s2、s3,需要將其拼接為一個(gè)字符串,有如下的幾種方式:
1.1 fmt.Sprintf
s := fmt.Sprintf("%s%s%s", s1, s2, s3)
1.2 +運(yùn)算符拼接
s := s1 + s2 + s3
1.3 strings.Join
s := strings.Join([]string{s1, s2, s3}, "")
1.4 strings.Builder
builder := strings.Builder{} builder.WriteString(s1) builder.WriteString(s2) builder.WriteString(s3) s := builder.String()
1.5 bytes.Buffer
buffer := bytes.Buffer{} buffer.WriteString(s1) buffer.WriteString(s2) buffer.WriteString(s3) s := buffer.String()
2、性能測試
上面介紹了5種字符串的拼接方式,那么它們的性能如何呢,接下來將對(duì)這五種字符串拼接進(jìn)行一個(gè)性能測試:
go版本:go1.21.0
如下為性能測試的結(jié)果,代碼將在最后面給出,總共有八種,分別為:
1.fmt.Sprintf
2.+
3.使用for循環(huán)和+拼接
4.strings.join
5.strings.Builder
6.strings.Builder(先使用Grow擴(kuò)容)
7.bytes.Buffer
8.bytes.Buffer(先使用Grow擴(kuò)容)
性能測試的結(jié)果如下(僅供參考):
拼接的字符串?dāng)?shù)量:3, 字符串長度:10, 性能如下
當(dāng)字符串?dāng)?shù)量和長度較小時(shí),性能從高到低:
+拼接
> strings.Builder(先Grow)
> strings.Join
> bytes.Buffer
> bytes.Buffer(先Grow)
> strings.Builder
> +拼接(使用for循環(huán))
> fmt.Sprintf
拼接的字符串?dāng)?shù)量:5, 字符串長度:128, 性能如下
當(dāng)字符串?dāng)?shù)量較多和長度較大時(shí),性能從高到低:
strings.Builder(先Grow)
> +拼接
> strings.Join
> bytes.Buffer(先Grow)
> fmt.Sprintf
> strings.Builder
> +拼接(使用for循環(huán))
> bytes.Buffer
從上面的壓測來看,直接使用+拼接字符串和使用strings.Builder(需要先grow)以及使用strings.Join的性能都是不錯(cuò)的。上面有幾個(gè)重點(diǎn)需要關(guān)注的點(diǎn):
1. 當(dāng)字符串?dāng)?shù)量較少長度較小時(shí),使用+來拼接字符串的效率非常高并且內(nèi)存分配次數(shù)為0(棧內(nèi)存分配)
2. 當(dāng)字符串?dāng)?shù)量較少長度較小時(shí),bytes.Grow使用和不使用區(qū)別不大 (bytes.Buffer的最小擴(kuò)容容量為64)
3. fmt.Sprintf的內(nèi)存分配次數(shù)最多(涉及大量的interface{}操作,導(dǎo)致逃逸)
接下來將從源碼的角度來分析它們的性能
3、源碼分析
注意:go的版本為1.21.0
3.1 +拼接
如果從感覺上來講,我們通常會(huì)認(rèn)為使用+
來拼接字符串肯定是最低效的,因?yàn)闀?huì)有多次字符串的拷貝,結(jié)果不然,接下來從源碼的角度進(jìn)行分析,看為什么使用+
來拼接字符串的效率是非常高的:
源碼位于runtime/string.go
下:
concatstrings實(shí)現(xiàn)了go的字符串+拼接,所有的字符串會(huì)被放入一個(gè)字符串切片中,并且會(huì)傳入一個(gè)大小為32字節(jié)的字符數(shù)組。
如果拼接后的字符串長度較小并且不會(huì)發(fā)生逃逸,那么就會(huì)在棧上創(chuàng)建出大小為32字節(jié)的字符數(shù)組。
步驟如下:
- 首先計(jì)算拼接后的字符串的長度;
- 如果編譯器可以確定拼接后的字符串不會(huì)發(fā)生逃逸,buf就不為nil,如果buf不為nil并且buf可以存放下拼接后的字符串,就使用buf
- 如果buf為nil或者大小不足,則會(huì)在堆上申請(qǐng)出一片可以存放下拼接后的字符串的空間,然后將字符串一個(gè)一個(gè)拷貝過去
// The constant is known to the compiler. // There is no fundamental theory behind this number. const tmpStringBufSize = 32 type tmpBuf [tmpStringBufSize]byte // concatstrings implements a Go string concatenation x+y+z+... func concatstrings(buf *tmpBuf, a []string) string { // 首先計(jì)算出拼接后的字符串的長度 idx := 0 l := 0 count := 0 for i, x := range a { n := len(x) if n == 0 { continue } if l+n < l { throw("string concatenation too long") } l += n count++ idx = i } if count == 0 { return "" } // 如果只有一個(gè)字符串并且它不在棧上或者我們的結(jié)果沒有轉(zhuǎn)義調(diào)用幀(但是f != nil),那么我們可以直接返回該字符串。 if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) { return a[idx] } s, b := rawstringtmp(buf, l) for _, x := range a { copy(b, x) b = b[len(x):] } return s } func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) { // 如果buf不為nil而且buf可以存放下拼接后的字符串,就直接使用buf if buf != nil && l <= len(buf) { b = buf[:l] s = slicebytetostringtmp(&b[0], len(b)) } else { // 否則在堆上分配一片區(qū)域 s, b = rawstring(l) } return } // 在堆上分配一片內(nèi)存,并且返回底層字符串結(jié)構(gòu)和切片結(jié)構(gòu),它們指向同一片內(nèi)存 func rawstring(size int) (s string, b []byte) { p := mallocgc(uintptr(size), nil, false) return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size) }
通過上面的源碼分析,可以得知,使用直接使用+
拼接字符串會(huì)先申請(qǐng)出一片內(nèi)存,然后將字符串一個(gè)一個(gè)拷貝過去,并且字符串有可能分配在棧上,因此效率非常高。
但是在使用for循環(huán)來拼接時(shí),由于編譯器無法確定最終的內(nèi)存空間大小,因此會(huì)發(fā)生多次拷貝,效率很低。
當(dāng)字符串比較小并且數(shù)量是已知的時(shí),使用+拼接字符串的效率很高,并且代碼可讀性更好。
3.2 strings.Builder
除了使用+來拼接字符串,通常string.Builder使用的也是非常多的,并且它的效率相比也是更高的,接下來看一下Builder的實(shí)現(xiàn)
在Builder中有一個(gè)字節(jié)切片的buf,每次在寫入時(shí)都會(huì)追加到buf中,當(dāng)buf容量不足時(shí),切片會(huì)自動(dòng)擴(kuò)容,但是在擴(kuò)容時(shí)會(huì)拷貝舊的切片,因此如果預(yù)先使用Grow來分配內(nèi)存,則可以減少擴(kuò)容時(shí)的拷貝開銷,從而提高效率。
另一個(gè)高效的原因是在使用String()獲取字符串時(shí)直接共用了切片的底層存儲(chǔ)數(shù)組,從而減少了一次數(shù)據(jù)的拷貝。因此Builder的所有api都是只能追加,不能修改的。
type Builder struct { addr *Builder // of receiver, to detect copies by value buf []byte } func (b *Builder) WriteString(s string) (int, error) { b.copyCheck() b.buf = append(b.buf, s...) return len(s), nil } func (b *Builder) grow(n int) { buf := bytealg.MakeNoZero(2*cap(b.buf) + n)[:len(b.buf)] copy(buf, b.buf) b.buf = buf } func (b *Builder) Grow(n int) { b.copyCheck() if n < 0 { panic("strings.Builder.Grow: negative count") } if cap(b.buf)-len(b.buf) < n { b.grow(n) } } // 返回的string和buf共用了同一片底層字符數(shù)組,減少了數(shù)據(jù)拷貝 func (b *Builder) String() string { return unsafe.String(unsafe.SliceData(b.buf), len(b.buf)) }
strings.Builder在獲取字符串時(shí)返回的string和buf共用同一片字符數(shù)組,因此減少了一次數(shù)據(jù)拷貝。在使用時(shí),使用grow預(yù)先分配內(nèi)存可以減少切片擴(kuò)容時(shí)的數(shù)據(jù)拷貝,提高性能,因此建議先使用Grow進(jìn)行預(yù)分配
3.3 strings.Join
在上面的性能測試中,Join的性能也很高,因?yàn)閟trings.join本身使用了strings.Builder,并且在拼接字符串之前使用Grow進(jìn)行了內(nèi)存預(yù)分配,因此效率也很高。
代碼很簡單,就不再介紹。
3.4 bytes.Buffer
bytes.Buffer和strings.Builder比較相似,但是通常用于處理字節(jié)數(shù)據(jù),而不是字符串。一個(gè)區(qū)別就是在使用String()方法來獲取字符串時(shí),有一次切片到字符串的拷貝,因此效率不如strings.Buffer但是當(dāng)字符串長度較小時(shí),bytes.Buffer的效率甚至比strings.Buffer要高。是因?yàn)?,Builder的擴(kuò)容是按照切片的擴(kuò)容策略來的,而Buffer的初始最小擴(kuò)容大小為64,也就是第一次擴(kuò)容最小大小為64,因此使用Grow和不使用的區(qū)別不大。
func (b *Buffer) String() string { if b == nil { // Special case, useful in debugging. return "<nil>" } return string(b.buf[b.off:]) } const smallBufferSize = 64 func (b *Buffer) grow(n int) int { ... if b.buf == nil && n <= smallBufferSize { b.buf = make([]byte, n, smallBufferSize) return 0 } ... }
3.5 fmt.Sprintf
fmt.Sprintf的實(shí)現(xiàn)較為復(fù)雜,并且使用了大量的interface{},會(huì)導(dǎo)致內(nèi)存逃逸,涉及到多次內(nèi)存分配,效率較低。如果是純字符串,通常不會(huì)使用fmt.Sprintf來進(jìn)行拼接,fmt.Sprintf可以對(duì)多種數(shù)據(jù)格式進(jìn)行字符串格式化。
總結(jié):
1.當(dāng)要拼接的多個(gè)字符串是已知并且數(shù)量較少時(shí),可以直接使用+來拼接,效率比較高而且可讀性更好
2、當(dāng)要拼接的字符串?dāng)?shù)量和長度未知時(shí),可以使用strings.Builder來拼接,并且預(yù)估字符串的大小使用Grow進(jìn)行預(yù)分配,效率較高
3、當(dāng)要拼接的字符串?dāng)?shù)量已知或者在拼接時(shí)需要加入分割字符串時(shí),可以使用strings.Join,效率較高,也很方便
4、在進(jìn)行字節(jié)數(shù)據(jù)處理時(shí)可以使用bytes.Buffer
5、當(dāng)要對(duì)包含多種格式的數(shù)據(jù)進(jìn)行字符串格式化時(shí)使用fmt.Sprintf,更加方便
壓測代碼:
package string_concats import ( "bytes" "fmt" "math/rand" "strings" "testing" "time" ) const dic = "qwertyuioplkjhgfdsazxcvbnmMNBVCXZASDFGHJKLPOIUYTREWQ0123456789" var defaultRand = rand.New(rand.NewSource(time.Now().UnixNano())) func RandString(n int) string { builder := strings.Builder{} builder.Grow(n) for i := 0; i < n; i++ { n := defaultRand.Intn(len(dic)) builder.WriteByte(dic[n]) } return builder.String() } var ( strs []string N = 5 Len = 128 ) func init() { for i := 0; i < N; i++ { strs = append(strs, RandString(Len)) } } // fmt.Sprintf func BenchmarkSprintf(b *testing.B) { for i := 0; i < b.N; i++ { _ = fmt.Sprintf("%s%s%s%s%s", strs[0], strs[1], strs[2], strs[3], strs[4]) } } // s1 + s2 + s3 func BenchmarkConcat(b *testing.B) { for i := 0; i < b.N; i++ { _ = strs[0] + strs[1] + strs[2] + strs[3] + strs[4] } } // for循環(huán)+ func BenchmarkForConcat(b *testing.B) { for i := 0; i < b.N; i++ { var s string for i := 0; i < len(strs); i++ { s += strs[i] } } } // strings.Join func BenchmarkJoin(b *testing.B) { for i := 0; i < b.N; i++ { _ = strings.Join(strs, "") } } // strings.Builder func BenchmarkBuilder(b *testing.B) { for i := 0; i < b.N; i++ { builder := strings.Builder{} for i := 0; i < len(strs); i++ { builder.WriteString(strs[i]) } _ = builder.String() } } // strings.Builder func BenchmarkBuilderGrowFirst(b *testing.B) { for i := 0; i < b.N; i++ { builder := strings.Builder{} n := 0 for i := 0; i < len(strs); i++ { n += len(strs[i]) } builder.Grow(n) for i := 0; i < len(strs); i++ { builder.WriteString(strs[i]) } _ = builder.String() } } // bytes.Buffer func BenchmarkBuffer(b *testing.B) { for i := 0; i < b.N; i++ { buffer := bytes.Buffer{} for i := 0; i < len(strs); i++ { buffer.WriteString(strs[i]) } _ = buffer.String() } } // bytes.Buffer func BenchmarkBufferGrowFirst(b *testing.B) { for i := 0; i < b.N; i++ { buffer := bytes.Buffer{} n := 0 for i := 0; i < len(strs); i++ { n += len(strs[i]) } buffer.Grow(n) for i := 0; i < len(strs); i++ { buffer.WriteString(strs[i]) } _ = buffer.String() } }
到此這篇關(guān)于go字符串拼接方式及性能比拼小結(jié)的文章就介紹到這了,更多相關(guān)go字符串拼接內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go?singleflight緩存雪崩源碼分析與應(yīng)用
這篇文章主要為大家介紹了go?singleflight緩存雪崩源碼分析與應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解
這篇文章主要介紹了Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12golang如何實(shí)現(xiàn)三元運(yùn)算符功能
這篇文章主要介紹了在其他一些編程語言中,如?C?語言,三元運(yùn)算符是一種可以用一行代碼實(shí)現(xiàn)條件選擇的簡便方法,那么在Go語言中如何實(shí)現(xiàn)類似功能呢,下面就跟隨小編一起學(xué)習(xí)一下吧2024-02-02深入學(xué)習(xí)Golang并發(fā)編程必備利器之sync.Cond類型
Go?語言的?sync?包提供了一系列同步原語,其中?sync.Cond?就是其中之一。本文將深入探討?sync.Cond?的實(shí)現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Cond,需要的可以參考一下2023-05-05golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼
這篇文章主要介紹了golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02