Why don't I recommend fmt.Sprintf

In daily programming, using fmt.Sprintf can easily splice strings, but did you know that fmt.Sprintf, which is harmless to humans and animals, is actually a performance killer.

In daily coding, we usually use the following methods to splice strings

// 1. + operator
str := str1+str2

// 2. fmt.Sprintf
str := fmt.Sprintf("%s %s",str1,str2)

// 3. strings.Builder
builder := strings.Builder{}
builder.WirteStirng(str1)
builder.WirteStirng(str2)
str := builder.String()

Which method is the most appropriate? Next, we will use benchmark for performance comparison and analysis

performance analysis

Construct string splicing code

var str1, str2 = "1", "2"
var strList = []string{str1, str2}

// MakeStrWithAdd + operator splicing
func MakeStrWithAdd() string {
	return str1 + ";" + str2
}

// MakeStrWithFmt fmt splice
func MakeStrWithFmt() string {
	return fmt.Sprintf("%s;%s", str1, str2)
}

// Makestrwithjoin splice
func MakeStrWithJoin() string {
	return strings.Join(strList, ";")
}

// Makestrwithbuilder
func MakeStrWithBuilder() string {
	var builder strings.Builder
	builder.WriteString(str1)
	builder.WriteString(str2)
	return builder.String()
}

The performance analysis code is as follows

var l = flag.Int("l", 10, "string length")

func TestMain(m *testing.M) {
	flag.Parse()
	str1 = string(make([]byte,*l))
	str2 = string(make([]byte,*l))
	strList = []string{str1, str2}
	m.Run()
}

func BenchmarkMakeStr(b *testing.B) {
	b.ResetTimer()
	b.Run("add", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithAdd()
		}
	})
	b.Run("fmt", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithFmt()
		}
	})
	b.Run("join", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithJoin()
		}
	})
	b.Run("builder", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithBuilder()
		}
	})
}

Test the benchmark results under different lengths of strings

# go test -bench=. -benchmem -l=10 		
BenchmarkMakeStr/add-12            43891863            23.7 ns/op            0 B/op            0 allocs/op
BenchmarkMakeStr/fmt-12            8019594             143 ns/op            64 B/op            3 allocs/op
BenchmarkMakeStr/join-12           28029506            41.6 ns/op            32 B/op           1 allocs/op
BenchmarkMakeStr/builder-12        18916494            62.5 ns/op            48 B/op           2 allocs/op

# go test -bench=. -benchmem -l=100
BenchmarkMakeStr/add-12            18775459                63.5 ns/op           208 B/op          1 allocs/op
BenchmarkMakeStr/fmt-12            7085766               168 ns/op             240 B/op          3 allocs/op
BenchmarkMakeStr/join-12           17654107                62.0 ns/op           208 B/op          1 allocs/op
BenchmarkMakeStr/builder-12        12204345                96.1 ns/op           336 B/op          2 allocs/op

# go test -bench=. -benchmem -l=1000
BenchmarkMakeStr/add-12            4781292               265 ns/op            2048 B/op          1 allocs/op
BenchmarkMakeStr/fmt-12            3045002               389 ns/op            2081 B/op          3 allocs/op
BenchmarkMakeStr/join-12           4464231               258 ns/op            2048 B/op          1 allocs/op
BenchmarkMakeStr/builder-12        3017283               369 ns/op            3072 B/op          2 allocs/op

# go test -bench=. -benchmem -l=10000
BenchmarkMakeStr/add-12            669583              1627 ns/op           20480 B/op          1 allocs/op
BenchmarkMakeStr/fmt-12            534519              2265 ns/op           20587 B/op          3 allocs/op
BenchmarkMakeStr/join-12           666103              1742 ns/op           20480 B/op          1 allocs/op
BenchmarkMakeStr/builder-12        496591              2558 ns/op           30720 B/op          2 allocs/op

It can be seen that the performance loss of string fmt splicing is the largest, and the simplest + splicing does save the most performance.

In addition, with the increase of string length, the advantage of + is no longer so obvious, mainly because the performance is mainly lost in memory copy

So it's a silver bullet? Let's make a benchmark for scenarios where the number of strings is not fixed

The modified code is as follows. fmt is not easy to handle for variable length slice, so it will not be tested temporarily.

var list []string
// MakeStrWithAdd2 + operator splicing
func MakeStrWithAdd2() (str string) {
	switch len(list) {
	case 0:
		return ""
	case 1:
		return list[0]
	}
	for _, item := range list {
		str += item
	}
	return
}

// Makestrwithjoin2 splice
func MakeStrWithJoin2() string {
	return strings.Join(list, "")
}

// Makestrwithbuilder2
func MakeStrWithBuilder2() string {
	var builder strings.Builder
	for _, item := range list {
		builder.WriteString(item)
	}
	return builder.String()
}

Modify the benchmark code as follows

var l = flag.Int("l", 10, "string length")
var n = flag.Int("str_number", 2, "string number")

func TestMain(m *testing.M) {
	flag.Parse()
	str1 = string(make([]byte, *l))
	str2 = string(make([]byte, *l))
	strList = []string{str1, str2}
	list = make([]string, *n)
	for i := 0; i < len(list); i++ {
		list[i] = "1234567890"
	}
	m.Run()
}

func BenchmarkMakeStr2(b *testing.B) {
	b.ResetTimer()
	b.Run("add", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithAdd2()
		}
	})
	b.Run("join", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithJoin2()
		}
	})
	b.Run("builder", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			MakeStrWithBuilder2()
		}
	})
}

The benchmark is as follows

# go test -bench=BenchmarkMakeStr2 -benchmem -str_number=2
BenchmarkMakeStr2/add-12           19187944                54.7 ns/op            32 B/op          1 allocs/op
BenchmarkMakeStr2/join-12          25535234                46.0 ns/op            32 B/op          1 allocs/op
BenchmarkMakeStr2/builder-12       17462570                70.4 ns/op            48 B/op          2 allocs/op
# go test -bench=BenchmarkMakeStr2 -benchmem -str_number=10
BenchmarkMakeStr2/add-12           2486616               475 ns/op             608 B/op          9 allocs/op
BenchmarkMakeStr2/join-12          8842040               129 ns/op             112 B/op          1 allocs/op
BenchmarkMakeStr2/builder-12       6427929               187 ns/op             240 B/op          4 allocs/op
# go test -bench=BenchmarkMakeStr2 -benchmem -str_number=20 
BenchmarkMakeStr2/add-12           1229839               972 ns/op            2224 B/op         19 allocs/op
BenchmarkMakeStr2/join-12          5468655               223 ns/op             208 B/op          1 allocs/op
BenchmarkMakeStr2/builder-12  		 4678641               254 ns/op             496 B/op          5 allocs/op
# go test -bench=BenchmarkMakeStr2 -benchmem -str_number=30
BenchmarkMakeStr2/add-12           625074              2057 ns/op            4912 B/op         29 allocs/op
BenchmarkMakeStr2/join-12          3677042               323 ns/op             320 B/op          1 allocs/op
BenchmarkMakeStr2/builder-12       3243637               367 ns/op            1008 B/op          6 allocs/op
	

It can be seen that with the increase of the number of strings, the performance degradation of + is particularly obvious. On the contrary, the performance of builder is much more stable.

conclusion

  • For fixed string scenarios, it is recommended to use + for string splicing. The code introduction is easy to implement

  • It is recommended to use builder for scenarios where strings become longer, especially those with too many strings.

  • In addition, fmt.Sprintf is not good for nothing. For scenes with many and complex occupancy types, it is recommended if it is not particularly harsh on performance. It is also a productivity to compare the code conciseness.

Keywords: Go Back-end

Added by smileyriley21 on Wed, 08 Dec 2021 20:24:16 +0200