// splicing_test.go
package splicing

import (
	"strconv"
	"strings"
	"testing"
)

func Str(str []string) string {
	var rst string
	for _, s := range str {
		rst += s
	}
	return rst
}

func BuilderStr(str []string) string {
	var builder strings.Builder
	for _, s := range str {
		builder.WriteString(s)
	}
	return builder.String()
}

func BenchmarkStr(b *testing.B) {
	srcStr := make([]string, 0, 100000)

	b.Run("Append-10000", func(b *testing.B) {
		for i := 0; i < 10000; i++ {
			srcStr = append(srcStr, strconv.Itoa(i%10))
		}
	})

	b.Run("Str-10000", func(b *testing.B) {
		Str(srcStr)
	})

	b.Run("BuilderStr-10000", func(b *testing.B) {
		BuilderStr(srcStr)
	})

	b.Run("Append-100000", func(b *testing.B) {
		for i := 0; i < 90000; i++ {
			srcStr = append(srcStr, strconv.Itoa(i%10))
		}
	})

	b.Run("Str-100000", func(b *testing.B) {
		Str(srcStr)
	})

	b.Run("BuilderStr-100000", func(b *testing.B) {
		BuilderStr(srcStr)
	})

}

测试结果

$ go test -bench=BenchmarkStr  -benchmem -memprofile=mem.prof -cpuprofile=cpu.prof
goos: linux
goarch: amd64
pkg: demo/splicing
BenchmarkStr/Append-10000-12            1000000000               0.000039 ns/op        0 B/op          0 allocs/op
BenchmarkStr/Str-10000-12               1000000000               0.177 ns/op           1 B/op          0 allocs/op
BenchmarkStr/BuilderStr-10000-12        1000000000               0.000242 ns/op        0 B/op          0 allocs/op
BenchmarkStr/Append-100000-12           1000000000               0.000333 ns/op        0 B/op          0 allocs/op
BenchmarkStr/Str-100000-12                     1        20121850900 ns/op       182358231216 B/op         600145 allocs/op
BenchmarkStr/BuilderStr-100000-12       1000000000               0.00258 ns/op         0 B/op          0 allocs/op
BenchmarkStrOnly-12                     1000000000               0.500 ns/op           5 B/op          0 allocs/op
PASS
ok      demo/splicing   35.097s
$ go tool pprof -http=:8080 mem.prof

内存消耗

demo/splicing.Str
/mnt/d/go/src/demo/splicing/splicing.go

  Total:    633.77GB   633.77GB (flat, cum) 199.94%
      1            .          .           package splicing 
      2            .          .            
      3            .          .           import "strings" 
      4            .          .            
      5            .          .           func Str(str []string) string { 
      6            .          .           	var rst string 
      7            .          .           	for _, s := range str { 
      8     633.77GB   633.77GB           		rst += s 
      9            .          .           	} 
     10            .          .           	return rst 
     11            .          .           } 
     12            .          .            
     13            .          .           func BuilderStr(str []string) string { 
     14            .          .           	var builder strings.Builder 
     15            .          .           	for _, s := range str { 
demo/splicing.BenchmarkStr.func4
/mnt/d/go/src/demo/splicing/splicing_test.go

  Total:     76.35MB    76.35MB (flat, cum) 0.024%
     20            .          .            
     21            .          .           	b.Run("BuilderStr-10000", func(b *testing.B) { 
     22            .          .           		BuilderStr(srcStr) 
     23            .          .           	}) 
     24            .          .            
     25            .          .           	b.Run("Append-100000", func(b *testing.B) { 
     26            .          .           		for i := 0; i < 90000; i++ { 
     27      76.35MB    76.35MB           			srcStr = append(srcStr, strconv.Itoa(i%10)) 
     28            .          .           		} 
     29            .          .           	}) 
     30            .          .            
     31            .          .           	b.Run("Str-100000", func(b *testing.B) { 
     32            .          .           		Str(srcStr) 
  • 从结果中可以看出,使用+=进行字符串拼接,会申请大量内存,并且速度非常慢。

原因分析

  • 使用 + 进行字符串拼接时,当 拼接 A 和 B 两个字符串时,需要申请另一个为A+B长度的空间C,每次都是如此。
  • 使用 strings.Builder 进行拼接时,内部实际上是使用的 []byte 存储,使用append操作,在不需要扩容时会在当前slice后追加,且每次是翻倍扩容,能减少扩容(申请新内存片段和复制等操作)次数。
  • (bytes.Buffer 与 strings.Builder 的差异)
    • bytes.Buffer
      func (b *Buffer) String() string {
          if b == nil {
              // Special case, useful in debugging.
              return "<nil>"
          }
          return string(b.buf[b.off:])
      }
      
    • strings.Builder
      // 会使用原buf的数据,而不会拷贝一份数据到返回值的字符串里面
      func (b *Builder) String() string {
          return *(*string)(unsafe.Pointer(&b.buf))
      }