欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

go字符串拼接方式及性能比拼小結(jié)

 更新時(shí)間:2024年01月09日 11:50:27   作者:CoreDump丶  
在golang中字符串的拼接方式有多種,本文將會(huì)介紹比較常用的幾種方式,并且對(duì)各種方式進(jìn)行壓測,具有一定的參考價(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)用

    這篇文章主要為大家介紹了go?singleflight緩存雪崩源碼分析與應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解

    Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解

    這篇文章主要介紹了Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • golang如何實(shí)現(xiàn)三元運(yùn)算符功能

    golang如何實(shí)現(xiàn)三元運(yùn)算符功能

    這篇文章主要介紹了在其他一些編程語言中,如?C?語言,三元運(yùn)算符是一種可以用一行代碼實(shí)現(xiàn)條件選擇的簡便方法,那么在Go語言中如何實(shí)現(xiàn)類似功能呢,下面就跟隨小編一起學(xué)習(xí)一下吧
    2024-02-02
  • go語言中排序sort的使用方法示例

    go語言中排序sort的使用方法示例

    golang中也實(shí)現(xiàn)了排序算法的包sort包,下面這篇文章就來給大家介紹了關(guān)于go語言中排序sort的使用方法,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-06-06
  • 深入學(xué)習(xí)Golang并發(fā)編程必備利器之sync.Cond類型

    深入學(xué)習(xí)Golang并發(fā)編程必備利器之sync.Cond類型

    Go?語言的?sync?包提供了一系列同步原語,其中?sync.Cond?就是其中之一。本文將深入探討?sync.Cond?的實(shí)現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Cond,需要的可以參考一下
    2023-05-05
  • 淺析Golang中類型嵌入的簡介與使用

    淺析Golang中類型嵌入的簡介與使用

    類型嵌入指的就是在一個(gè)類型的定義中嵌入了其他類型,Go?語言支持兩種類型嵌入,分別是接口類型的類型嵌入和結(jié)構(gòu)體類型的類型嵌入,下面我們就來詳細(xì)一下類型嵌入的使用吧
    2023-11-11
  • Golang工具庫viper的使用教程

    Golang工具庫viper的使用教程

    viper?是?go?項(xiàng)目中用來讀取配置文件的庫,支持讀取?yaml、toml、json、hcl、env?等格式的配置文件,下面就來和大家聊聊它的具體使用吧
    2023-07-07
  • golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼

    golang 生成二維碼海報(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
  • Golang性能優(yōu)化的技巧分享

    Golang性能優(yōu)化的技巧分享

    性能優(yōu)化的前提是滿足正確可靠、簡潔清晰等質(zhì)量因素,針對(duì)?Go語言特性,本文為大家整理了一些Go語言相關(guān)的性能優(yōu)化建議,感興趣的可以了解一下
    2023-07-07
  • 詳解Golang中g(shù)omock的使用場景和方法

    詳解Golang中g(shù)omock的使用場景和方法

    gomock是Go編程語言的模擬框架, 它與Go的內(nèi)置測試包很好地集成在一起,但也可以在其他上下文中使用,本文主要介紹了gomock的使用場景和方法,感興趣的可以了解下
    2024-10-10

最新評(píng)論