【Go开源宝藏】基于 Golang 语法的性能调优技巧(字符串拼接)
字符串拼接
我们一般使用字符串拼接方式有三种
- 直接拼接
str += "sum"
- fmt.Sprintf(“%s”,xxxxx)
- 使用string.Builder
- 使用bytes.Builder
- 使用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.Builder
、bytes.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
转换成了字符串类型返回了回来。
第五个:
而第五个,毫无疑问事先分配好了内存,肯定是最快的了。
参考链接
文章来源: blog.csdn.net,作者:小生凡一,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_45304503/article/details/125837527
- 点赞
- 收藏
- 关注作者
评论(0)