go字符串拼接方式及性能比拼

news2024/12/23 21:04:08

在golang中字符串的拼接方式有多种,本文将会介绍比较常用的几种方式,并且对各种方式进行压测,以此来得到在不同场景下更适合使用的方案。

文章目录

    • 1、go字符串的几种拼接方式
      • 1.1 `fmt.Sprintf`
      • 1.2 `+运算符拼接`
      • 1.3 `strings.Join`
      • 1.4 `strings.Builder`
      • 1.5 `bytes.Buffer`
    • 2、性能测试
    • 3、源码分析
      • 3.1 +拼接
      • 3.2 strings.Builder
      • 3.3 strings.Join
      • 3.4 bytes.Buffer
      • 3.5 fmt.Sprintf
    • 总结:
    • 压测代码:

1、go字符串的几种拼接方式

比如对于三个字符串,s1、s2、s3,需要将其拼接为一个字符串,有如下的几种方式:

1.1 fmt.Sprintf

s := fmt.Sprintf("%s%s%s", s1, s2, s3)

1.2 +运算符拼接

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种字符串的拼接方式,那么它们的性能如何呢,接下来将对这五种字符串拼接进行一个性能测试:

go版本:go1.21.0

如下为性能测试的结果,代码将在最后面给出,总共有八种,分别为:

1.fmt.Sprintf

2.+

2.使用for循环和+拼接

4.strings.join

5.strings.Builder

6.strings.Builder(先使用Grow扩容)

7.bytes.Buffer

8.bytes.Buffer(先使用Grow扩容)

 

性能测试的结果如下(仅供参考):

拼接的字符串数量:3, 字符串长度:10, 性能如下

在这里插入图片描述

当字符串数量和长度较小时,性能从高到低:

+拼接 > strings.Builder(先Grow) > strings.Join > bytes.Buffer > bytes.Buffer(先Grow) > strings.Builder > +拼接(使用for循环) > fmt.Sprintf

 

拼接的字符串数量:5, 字符串长度:128, 性能如下

在这里插入图片描述

当字符串数量较多和长度较大时,性能从高到低:

strings.Builder(先Grow) > +拼接 > strings.Join > bytes.Buffer(先Grow) > fmt.Sprintf > strings.Builder > +拼接(使用for循环) > bytes.Buffer

从上面的压测来看,直接使用+拼接字符串和使用strings.Builder(需要先grow)以及使用strings.Join的性能都是不错的。
上面有几个重点需要关注的点:

1. 当字符串数量较少长度较小时,使用+来拼接字符串的效率非常高并且内存分配次数为0(栈内存分配)
2. 当字符串数量较少长度较小时,bytes.Grow使用和不使用区别不大 (bytes.Buffer的最小扩容容量为64)
3. fmt.Sprintf的内存分配次数最多(涉及大量的interface{}操作,导致逃逸)

接下来将从源码的角度来分析它们的性能

3、源码分析

注意:go的版本为1.21.0

3.1 +拼接

       如果从感觉上来讲,我们通常会认为使用+来拼接字符串肯定是最低效的,因为会有多次字符串的拷贝,结果不然,接下来从源码的角度进行分析,看为什么使用+来拼接字符串的效率是非常高的:

源码位于runtime/string.go下:

concatstrings实现了go的字符串+拼接,所有的字符串会被放入一个字符串切片中,并且会传入一个大小为32字节的字符数组。

如果拼接后的字符串长度较小并且不会发生逃逸,那么就会在栈上创建出大小为32字节的字符数组。

步骤如下:

  1. 首先计算拼接后的字符串的长度;
  2. 如果编译器可以确定拼接后的字符串不会发生逃逸,buf就不为nil,如果buf不为nil并且buf可以存放下拼接后的字符串,就使用buf
  3. 如果buf为nil或者大小不足,则会在堆上申请出一片可以存放下拼接后的字符串的空间,然后将字符串一个一个拷贝过去
// 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 {
	// 首先计算出拼接后的字符串的长度
    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 ""
	}

    // 如果只有一个字符串并且它不在栈上或者我们的结果没有转义调用帧(但是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 {
        // 否则在堆上分配一片区域
		s, b = rawstring(l)
	}
	return
}

// 在堆上分配一片内存,并且返回底层字符串结构和切片结构,它们指向同一片内存
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)
}

通过上面的源码分析,可以得知,使用直接使用+拼接字符串会先申请出一片内存,然后将字符串一个一个拷贝过去,并且字符串有可能分配在栈上,因此效率非常高。

但是在使用for循环来拼接时,由于编译器无法确定最终的内存空间大小,因此会发生多次拷贝,效率很低。

当字符串比较小并且数量是已知的时,使用+拼接字符串的效率很高,并且代码可读性更好。

3.2 strings.Builder

除了使用+来拼接字符串,通常string.Builder使用的也是非常多的,并且它的效率相比也是更高的,接下来看一下Builder的实现

在Builder中有一个字节切片的buf,每次在写入时都会追加到buf中,当buf容量不足时,切片会自动扩容,但是在扩容时会拷贝旧的切片,因此如果预先使用Grow来分配内存,则可以减少扩容时的拷贝开销,从而提高效率。

另一个高效的原因是在使用String()获取字符串时直接共用了切片的底层存储数组,从而减少了一次数据的拷贝。因此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共用了同一片底层字符数组,减少了数据拷贝
func (b *Builder) String() string {
	return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}

strings.Builder在获取字符串时返回的string和buf共用同一片字符数组,因此减少了一次数据拷贝。在使用时,使用grow预先分配内存可以减少切片扩容时的数据拷贝,提高性能,因此建议先使用Grow进行预分配

3.3 strings.Join

在上面的性能测试中,Join的性能也很高,因为strings.join本身使用了strings.Builder,并且在拼接字符串之前使用Grow进行了内存预分配,因此效率也很高。

代码很简单,就不再介绍。

3.4 bytes.Buffer

bytes.Buffer和strings.Builder比较相似,但是通常用于处理字节数据,而不是字符串。一个区别就是在使用String()方法来获取字符串时,有一次切片到字符串的拷贝,因此效率不如strings.Buffer
但是当字符串长度较小时,bytes.Buffer的效率甚至比strings.Buffer要高。是因为,Builder的扩容是按照切片的扩容策略来的,而Buffer的初始最小扩容大小为64,也就是第一次扩容最小大小为64,因此使用Grow和不使用的区别不大。

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的实现较为复杂,并且使用了大量的interface{},会导致内存逃逸,涉及到多次内存分配,效率较低。如果是纯字符串,通常不会使用fmt.Sprintf来进行拼接,fmt.Sprintf可以对多种数据格式进行字符串格式化。

总结:

1.当要拼接的多个字符串是已知并且数量较少时,可以直接使用+来拼接,效率比较高而且可读性更好

2、当要拼接的字符串数量和长度未知时,可以使用strings.Builder来拼接,并且预估字符串的大小使用Grow进行预分配,效率较高

3、当要拼接的字符串数量已知或者在拼接时需要加入分割字符串时,可以使用strings.Join,效率较高,也很方便

4、在进行字节数据处理时可以使用bytes.Buffer

5、当要对包含多种格式的数据进行字符串格式化时使用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循环+
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()
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1041240.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用redis实现分布式锁

为什么需要分布式锁 在一个分布式系统中&#xff0c;也会涉及多个节点访问同一个公共资源的情况&#xff0c;此时就需要通过锁来做互斥控制&#xff0c;避免出现类似于“线程安全”的问题&#xff0c;而java的synchronized这样的锁只能在当前进程中生效&#xff0c;在分布式的…

【Go】Golang环境配置与语法基础

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Golang环境配置与示例。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路…

【Pycharm/Anaconda配置环境记录】

文章目录 1、Pytorch配置2、mmcv配置 1、Pytorch配置 查看虚拟环境创建虚拟环境 conda env list&#xff1a;查看虚拟环境 conda create --name env_name python3.7&#xff1a;创建虚拟环境 conda activate env_name&#xff1a;激活/进入该虚拟环境 查看自己的CUDA版本以及P…

element-plus-自定义Dialog样式

实现如下 <template><h3>dialog3 test 全局</h3><el-button type"primary" size"default" click"show">全局弹窗</el-button><div class""><!-- 弹窗 --><el-dialog v-model"visi…

使用 Matter-SDK 快速搭建 Matter 环境 (Linux)

Matter 作为一个统一的智能家居互联协议&#xff0c;凭借其高兼容性的特点&#xff0c;正逐渐打破各个智能家居之间的壁垒。乐鑫作为在 Matter 项目发布之初的早期成员&#xff0c;提供了一套开源、完整、易用的 Matter-SDK。 乐鑫的 Matter-SDK 是建立在开源 Matter-SDK 之上…

MySQL - order by排序查询 (查询操作 四)

功能介绍&#xff1a;order by&#xff1a;对查询结果进行排序&#xff0c;执行顺序在查询完结果执行&#xff08;比如where后&#xff09; 排序方式&#xff1a;ASC&#xff1a;升序&#xff08;默认&#xff0c;不用指定&#xff09; DESC&#xff1a;降序 语法&#x…

Android跨进程通信:Binder机制原理

目录 1. Binder到底是什么&#xff1f; 2. 知识储备 2.1 进程空间划分 2.2 进程隔离 & 跨进程通信&#xff08; IPC &#xff09; 2.3 内存映射 2.3.1 作用 2.3.2 实现过程 2.3.3 特点 2.3.4 应用场景 2.3.5 实例讲解 ① 文件读 / 写操作 ② 跨进程通信 3. Bi…

【Java 基础篇】Java 图书管理系统详解

介绍 图书管理系统是一种用于管理图书信息、借阅记录、用户信息等的软件系统。它可以帮助图书馆、书店或个人管理和组织图书资源&#xff0c;提供了方便的借阅和查询功能。在这篇博客中&#xff0c;我们将详细介绍如何使用Java编程语言创建一个简单的图书管理系统。 功能需求…

SpringMVC 学习(一)Servlet

1. Hello Servlet (1) 创建父工程 删除src文件夹 引入一些基本的依赖 <!--依赖--> <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test<…

大模型lora微调-chatglm2

通义千问大模型微调源码&#xff08;chatglm2 微调失败&#xff0c;训练通义千问成功&#xff09;&#xff1a;GitHub - hiyouga/LLaMA-Efficient-Tuning: Easy-to-use LLM fine-tuning framework (LLaMA-2, BLOOM, Falcon, Baichuan, Qwen, ChatGLM2)Easy-to-use LLM fine-tun…

STM32存储左右互搏 I2C总线读写FRAM MB85RC1M

STM32存储左右互搏 I2C总线读写FRAM MB85RC1M 在较低容量存储领域&#xff0c;除了EEPROM的使用&#xff0c;还有铁电存储器FRAM的使用&#xff0c;相对于EEPROM, 同样是非易失性存储单元&#xff0c;FRAM支持更高的访问速度&#xff0c; 其主要优点为没有EEPROM持续写操作跨页…

day06_循环

今日内容 零、 复习昨日 一、循环 二、流程控制关键词 零、 复习昨日 8个基本数据类型 变量的使用步骤 1)声明2)赋值3)使用 声明,数据类型 变量名 不一定非得是基本类型 int a; String s; Scanner scanner;赋值,只要符合类型(能默认转换)就能赋值 int a 1; double d 1; Scann…

uni-app问题记录

一、启动问题记录 1. 报错1 解决办法: 开启微信开发者工具服务端口 2. 报错2:调用getLocation获取位置信息时报错以下内容 {errMsg: “getLocation:fail the api need to be declared in the requiredPrivateInfos field in app.json/ext.json”} 解决办法: manifest.json文…

到广阔的边缘市场去,浪潮信息首次发布全栈边缘计算软硬件新品

出品 | CSDN 云计算 智慧时代&#xff0c;一切皆计算&#xff0c;早已不再是一句口号。据国际研究机构 IDC 数据显示&#xff0c;2023 年超过 50%的企业新增 IT 基础设施会部署在边缘&#xff0c;而 Gartner 研究显示&#xff0c;到 2025 年&#xff0c;超过 75%的数据生成和数…

【ROS入门】使用 ROS 服务(Service)机制实现同步请求与答复

文章结构 任务要求话题模型实现步骤自定义srv定义srv文件编辑配置文件编译 自定义srv调用vscode配置编写服务端实现编写客户端实现 执行启动roscore编译启动客户端和服务端编译启动roscore启动节点 任务要求 编写代码实现 ROS 中的服务请求与答复: 创建服务端&#xff0c;注册…

vue中使用富文本编辑器,@tinymce/tinymce-vue

富文本就是在后台管理系统中常见的录入带格式的内容&#xff0c;如&#xff1a;表格&#xff0c;字体加粗&#xff0c;斜体&#xff0c;文字颜色等等&#xff0c;就像一个word一样。类似于这样的效果&#xff1a; 我们使用通用在线编辑器tinymce。支持vue和react。 1. 安装 np…

opencv形态学-腐蚀

opencv形态学-腐蚀 腐蚀就是取每一个位置结构元领域内最小值作为该位置的输出灰度值&#xff1b; 结构元有很多&#xff0c;一般采用矩形&#xff0c;圆 解析 下图左测是原始图片的灰阶&#xff0c;右边是经过3X3矩形腐蚀后的结果&#xff0c;我们拿19,44,99进行分析&#…

Flink CDC MySQL同步MySQL错误记录

1、启动 Flink SQL [appuserwhtpjfscpt01 flink-1.17.1]$ bin/sql-client.sh2、新建源表 问题1&#xff1a;Encountered “(” 处理方法&#xff1a;去掉int(11)&#xff0c;改为int Flink SQL> CREATE TABLE t_user ( > uid int(11) NOT NULL AUTO_INCREMENT COMME…

关于DDR协议的一些操作的理解4

address 1.DDR中的地址&#xff0c;下表中的*4/ *8/ *16表示的是颗粒位宽。不同位宽的颗粒的行列地址的分步是不一样的。图中的page size表示的就是一行所存储的内容&#xff0c;以64MB*16格式为例&#xff0c;一行一共有10列&#xff0c;每一列存储16bit&#xff0c;也就是2B…

每日一题 146. LRU 缓存

难度&#xff1a;中等 由于周日没做&#xff0c;今天又是困难题&#xff0c;所以假装今天是周日 思路&#xff1a; 在字典结构的基础之上完成三个要求显然题目要求构建一个有序字典&#xff08;当然不使用OrderedDict&#xff09;&#xff0c;由于 key 是唯一的&#xff0c;…