Go 代码性能优化小技巧_字符串拼接日常开发中经常使用到的代码性能优化小技巧~

Go 代码性能优化小技巧_i++_02

前言

本文总结了 Go 语言日常开发中经常使用到的代码性能优化小技巧,每个知识点都可以深挖其原理,但本文只给出结论,并附上 benchmark 结果,从测试数据来直观地看性能差异,具体的原理分析可以看每一小节给出的参考文章。

前置知识:Go benchmark 详解

切片预分配容量

如果我们事先知道数据量大小,则可以提前分配切片容量,避免扩容时的元素拷贝。

before

func BenchmarkBefore(b *testing.B) {
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		var s []int // 未指定切片容量
		b.StartTimer()
		for j := 0; j < 10000; j++ {
			s = append(s, j)
		}
	}
}

after

func BenchmarkAfter(b *testing.B) {
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		s := make([]int, 0, 1000) // 预先指定切片容量
		b.StartTimer()
		for j := 0; j < 10000; j++ {
			s = append(s, j)
		}
	}
}

benchmark

$ go test -bench="." -benchmem
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkBefore-4          16641             76656 ns/op          386304 B/op         20 allocs/op
BenchmarkAfter-4           21256             56961 ns/op          334465 B/op          7 allocs/op
PASS
ok      learnGolang     6.305s

reference

Go 切片

Go slice 扩容机制分析

大量字符串拼接

Go 语言的字符串为不可变类型,使用 + 拼接字符串会创建一个新的对象,在大量字符串拼接的场景下,使用 strings.Builder 可以显著提升性能。

before

func Before(str string, n int) string {
	var s string
	for i := 0; i < n; i++ {
		s += str // 直接使用 + 来拼接
	}
	return s
}

after

func After(str string, n int) string {
	var buf strings.Builder
	for i := 0; i < n; i++ {
		buf.WriteString(str) // 使用 strings.Builder 优化拼接
	}
	return buf.String()
}

benchmark

var (
	str = "hello,世界"
	n   = 1000
)

func BenchmarkBefore(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Before(str, n)
	}
}

func BenchmarkAfter(b *testing.B) {
	for i := 0; i < b.N; i++ {
		After(str, n)
	}
}
$ go test -bench="." -benchmem
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkBefore-4            756           1391783 ns/op         6366122 B/op        999 allocs/op
BenchmarkAfter-4           82411             15351 ns/op           53232 B/op         15 allocs/op
PASS
ok      learnGolang     2.705s

reference

Go string 详解

字符串拼接性能及原理

正则表达式预编译

如果正则表达式是确定不变的,则可以将其定义为全局变量并预先编译,避免每次使用时现编译。

before

func ParseIPv4Before(ip string) bool {
	re := regexp.MustCompile(`(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`)
	return re.MatchString(ip)
}

after

var IPv4Regex = regexp.MustCompile(`(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`)func ParseIPv4After(ip string) bool {	return IPv4Regex.MatchString(ip)}

benchmark

func BenchmarkBefore(b *testing.B) {	for i := 0; i < b.N; i++ {		ParseIPv4Before("192.168.2.255")	}}func BenchmarkAfter(b *testing.B) {	for i := 0; i < b.N; i++ {		ParseIPv4After("192.168.2.255")	}}
$ go test -bench="." -benchmemgoos: windowsgoarch: amd64pkg: learnGolangBenchmarkBefore-4          80751             12892 ns/op           11046 B/op         83 allocs/opBenchmarkAfter-4         3840087               371 ns/op               0 B/op          0 allocs/opPASSok      learnGolang     2.992s