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.