Golang字符串拼接的性能以及原理详解
作者:Jeson-Sun
1.字符串高效拼接
在go语言中,字符串(string)是不可变的,因此字符串之间的拼接实际上是创建了一个新的字符串。如果频繁的进行字符串拼接,那将会对性能产生严重的影响!
1.1常见的拼接方式
(1)使用 +
func plusConcat(n int, str string) string { s := "" for i := 0; i < n; i++ { s += str } return s }
(2)使用fmt.Sprintf
func sprintfConcat(n int, str string) string { s := "" for i := 0; i < n; i++ { s = fmt.Sprintf("%s%s", s, str) } return s }
(3) 使用strings.Builder
func builderConcat(n int, str string) string { var builder strings.Builder for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() }
(4) 使用bytes.Buffer
func bufferConcat(n int, str string) string { buffer := new(bytes.Buffer) for i := 0; i < n; i++ { buffer.WriteString(str) } return buffer.String() }
(5) 使用[] byte
func byteConcat(n int, str string) string { buf := make([]byte, 0, n*len(str)) for i := 0; i < n; i++ { buf = append(buf, str...) } return string(buf) }
1.2使用benchmark进行性能对比
测试代码:
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randomString(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } func benchmark(b *testing.B, f func(int, string) string) { var str = randomString(10) for i := 0; i < b.N; i++ { f(10000, str) } } func BenchmarkPlusConcat(b *testing.B) { benchmark(b, plusConcat) } func BenchmarkSprintfConcat(b *testing.B) { benchmark(b, sprintfConcat) } func BenchmarkBuilderConcat(b *testing.B) { benchmark(b, builderConcat) } func BenchmarkBufferConcat(b *testing.B) { benchmark(b, bufferConcat) } func BenchmarkByteConcat(b *testing.B) { benchmark(b, byteConcat) }
测试结果:
$ go test -bench="Concat$" -benchmem .
goos: darwin
goarch: amd64
pkg: example
BenchmarkPlusConcat-8 19 56 ms/op 530 MB/op 10026 allocs/op
BenchmarkSprintfConcat-8 10 112 ms/op 835 MB/op 37435 allocs/op
BenchmarkBuilderConcat-8 8901 0.13 ms/op 0.5 MB/op 23 allocs/op
BenchmarkBufferConcat-8 8130 0.14 ms/op 0.4 MB/op 13 allocs/op
BenchmarkByteConcat-8 17379 0.07 ms/op 0.2 MB/op 2 allocs/op
PASS
ok example 8.627s
总结: 通过对比,发现fmt.sprintf()
和+
的性能最低,和其他方法相比,性能低了差不多1000倍,且占用内存也比其他方法高了1000倍;而其他三者的性能和占用内存相差不多;性能最高的方法是[]byte,因为提前分配了足够的内存,所以拼接是不会进行字符串的拷贝与内存的重新分配,固效果最佳。
1.3字符串拼接最终建议
综合易用性和性能,一般推荐使用 strings.Builder
来拼接字符串。
官方解释:
A Builder is used to efficiently build a string using Write methods.
It minimizes memory copying.
如果对 strings.Builder
进行内存预分配,性能还可以再次提升。可以使用Grow()来对内存进行预分配。
如:
func builderConcat(n int, str string) string { var builder strings.Builder builer.Grow(n*len(str)) for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() }
2.性能背后的原理
2.1 比较 strings.Builder和 +
strings.Builder 和 + 性能和内存消耗差距如此巨大,是因为两者的内存分配方式不一样。
- 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用
+
拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。假设一个字符串大小为 10 byte,拼接 1w 次,需要申请的内存大小为:
10 + 2 * 10 + 3 * 10 + … + 10000 * 10 byte = 500 MB
- 而
strings.Builder
、bytes.Buffer
,包括切片[]byte
的内存是以倍数申请的。例如,初始大小为 0,当第一次写入大小为 10 byte 的字符串时,则会申请大小为 16 byte 的内存(恰好大于 10 byte 的 2 的指数),第二次写入 10 byte 时,内存不够,则申请 32 byte 的内存,第三次写入内存足够,则不申请新的,以此类推。在实际过程中,超过一定大小,比如 2048 byte 后,申请策略上会有些许调整。
2.2 比较 strings.Builder 和 bytes.Buffer
strings.Builder
和 bytes.Buffer
底层都是 []byte 数组,但 strings.Builder
性能比 bytes.Buffer
略快约 10% 。一个比较重要的区别在于,bytes.Buffer
转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder
直接将底层的[]byte
转换成了字符串类型返回了回来。
总结
到此这篇关于Golang字符串拼接的性能以及原理详解的文章就介绍到这了,更多相关Golang字符串拼接性能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!