【Go开源宝藏】基于 Golang 语法的性能调优技巧(字符串拼接)

举报
小生凡一 发表于 2022/07/17 23:15:07 2022/07/17
【摘要】 文章目录 字符串拼接参考链接 字符串拼接 我们一般使用字符串拼接方式有三种 直接拼接 str += "sum"fmt.Sprintf(“%s”,xxxxx)使用string.Builde...

字符串拼接

我们一般使用字符串拼接方式有三种

  1. 直接拼接 str += "sum"
  2. fmt.Sprintf(“%s”,xxxxx)
  3. 使用string.Builder
  4. 使用bytes.Builder
  5. 使用byte进行拼接

我们先来写一个benchmark去测试每一种字符串拼接的情况。

// 1. 直接拼接
func BenchmarkString(b *testing.B) {
	elems := make([]string, 100000, 100000)
	for i := 0; i < len(elems); i++ {
		elems[i] = strconv.Itoa(i)
	}
	sum := ""
	length := len(elems)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < length; j++ {
			sum += elems[j]
		}
	}
	b.StopTimer()
}

// 2. fmt.Sprintf("%s",xxxxx)
func BenchmarkFmtSprintfString(b *testing.B) {
	elems := make([]string, 100000, 100000)
	for i := 0; i < len(elems); i++ {
		elems[i] = strconv.Itoa(i)
	}
	length := len(elems)
	sum := ""
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < length; j++ {
			sum += fmt.Sprintf("%s", elems[j])
		}
	}
	b.StopTimer()
}

// 3. string.Builder
func BenchmarkBuilderString(b *testing.B) {
	elems := make([]string, 100000, 100000)
	for i := 0; i < len(elems); i++ {
		elems[i] = strconv.Itoa(i)
	}
	var builder strings.Builder
	length := len(elems)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < length; j++ {
			builder.WriteString(elems[j])
		}
	}
	b.StopTimer()
}

// 4. bytes.Builder
func BenchmarkByteBufferString(b *testing.B) {
	elems := make([]string, 100000, 100000)
	for i := 0; i < len(elems); i++ {
		elems[i] = strconv.Itoa(i)
	}
	buffer := new(bytes.Buffer)
	length := len(elems)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < length; j++ {
			buffer.WriteString(elems[j])
		}
	}
	b.StopTimer()
}

// 5. byte 拼接
func BenchmarkByteConcatString(b *testing.B) {
	elems := make([]string, 100000, 100000)
	for i := 0; i < len(elems); i++ {
		elems[i] = strconv.Itoa(i)
	}
	length := len(elems)
	buf := make([]byte, 0, len(elems))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < length; j++ {
			buf = append(buf, elems[j]...)
		}
	}
	b.StopTimer()
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 执行
go test -bench="String$" -benchmem . 

  
 
  • 1

or

gobench string_test.go

  
 
  • 1
  • 结果
测试函数				同等时间内执行了多少次 				总共的执行时间
1. 直接拼接
BenchmarkString
BenchmarkString-8                      1        8930752200 ns/op
2. fmt.Sprintf
BenchmarkFmtSprintf
BenchmarkFmtSprintf-8                  1        8773251900 ns/op
3. StringBuilder
BenchmarkBuilder
BenchmarkBuilder-8                   991           1199075 ns/op
4. ByteBuffer
BenchmarkByteBuffer
BenchmarkByteBuffer-8               1458            945317 ns/op
5. ByteConcat
BenchmarkByteConcat
BenchmarkByteConcat-8               2010            803761 ns/op

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我们一个一个来进行分析

第一个:

在go语言中,字符串(string类型)是不可变的(如果我们需要改变字符串,就要转成byte类型,再转回string类型),因此字符串之间的拼接实际上是创建了一个新的字符串,就会不断开辟内存空间,如果频繁进行字符串拼接,就会对性能产生严重的影响。

字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接两个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。

假设一个字符串大小为 10 byte,拼接 1w 次,需要申请的内存大小为:

10 + 2 * 10 + 3 * 10 + … + 10000 * 10 byte = 500 MB

  
 
  • 1

第二个:
Go语言 fmt.Sprintf 的底层实现中,用到了反射的机制。那为什么反射会慢呢?

在 Go 里面的反射是这样设计的:

type_ := reflect.TypeOf(obj) 
field, _ := type_.FieldByName("fan")

  
 
  • 1
  • 2

这里取出来的** field 对象reflect.StructField 类型,但是它没有办法用来取得对应对象上的值**。如果要取值,得用另外一套对object进行反射,而不是type的反射

type_ := reflect.ValueOf(obj) 
value := type_.FieldByName("fan")

  
 
  • 1
  • 2

这里取出来的 value 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到 GC

当我们涉及到了大量的内存开辟的时候,就会使得系统变得异常慢。

第三个:

strings.Builderbytes.Buffer,包括切片 []byte 的内存 是以倍数申请的。

例如:

初始大小为 0
第一次写入大小为 10 byte 的字符串时,则会申请大小为 16 byte ( 2^4) 的内存(恰好大于 10 byte 的 2 的指数)。
第二次写入 10 byte 时,20byte > 16 byte,内存不够,则申请 32 byte (2^5) 的内存。
第三次写入内存足够,30 byte < 32 byte,则不申请新的,以此类推。
在实际过程中,超过一定大小,比如 2048 byte 后,申请策略上会有些许调整。

所以内存的复用是性能极高的!

第四个:

strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,但 strings.Builder 性能比 bytes.Buffer 略快约 10% 。
一个比较重要的区别在于:
bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder直接将底层的[]byte转换成了字符串类型返回了回来。

第五个:

而第五个,毫无疑问事先分配好了内存,肯定是最快的了。

参考链接

Go语言的反射原理
Go语言的string拼接

文章来源: blog.csdn.net,作者:小生凡一,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/weixin_45304503/article/details/125837527

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。