go之字符串拼接使用->性能->背后原因

前言

因为之前是搞java的在使用字符串拼接的时候避免一直+产生新的String对象,就使用StringBuilder或者StringBuffer。最近在使用go的时候也需要字符串拼接,所以查了下go的字符串拼接的方式,发现有很多种,下面我们将从使用->直观的性能比较->背后的原因来娓娓道来。

使用

fmt.Sprintf

func useFmtSprintf(s1 string, s2 string) {
   fmt.Println(fmt.Sprintf("%s-%s", s1, s2))
}

+

func useAdd(s1 string, s2 string) {
   fmt.Println(s1 + s2)
}

strings.Join

func useStringJoin(s1 string, s2 string) {
   fmt.Println(strings.Join([]string{s1, s2}, ""))
}

bytes.Buffer

func useBuffer(s1 string, s2 string) {
   buffer := bytes.Buffer{}
   buffer.WriteString(s1)
   buffer.WriteString(s2)
   fmt.Println(buffer.String())
}

strings.Builder

func useStringBuilder(s1 string, s2 string) {
   builder := strings.Builder{}
   builder.WriteString(s1)
   builder.WriteString(s2)
   fmt.Println(builder.String())
}

性能对比

package test

import (
   "bytes"
   "fmt"
   "strings"
   "testing"
)

// fmt.Printf
func BenchmarkFmtSprintfMore(b *testing.B) {
   var s string
   for i := 0; i < b.N; i++ {
      s += fmt.Sprintf("%s%s", "hello", "world")
   }
   fmt.Errorf(s)
}

// 加号 拼接
func BenchmarkAddMore(b *testing.B) {
   var s string
   for i := 0; i < b.N; i++ {
      s += "hello" + "world"
   }
   fmt.Errorf(s)
}

// strings.Join
func BenchmarkStringsJoinMore(b *testing.B) {

   var s string
   for i := 0; i < b.N; i++ {
      s += strings.Join([]string{"hello", "world"}, "")

   }
   fmt.Errorf(s)
}

// bytes.Buffer
func BenchmarkBufferMore(b *testing.B) {

   buffer := bytes.Buffer{}
   for i := 0; i < b.N; i++ {
      buffer.WriteString("hello")
      buffer.WriteString("world")

   }
   fmt.Errorf(buffer.String())
}

func BenchmarkStringBuilderMore(b *testing.B) {
   builder := strings.Builder{}
   for i := 0; i < b.N; i++ {
      builder.WriteString("hello")
      builder.WriteString("world")

   }
   fmt.Errorf(builder.String())
}

结果

➜  test go test -bench="." -count=3
goos: darwin
goarch: arm64
pkg: StudyProject/src/second/test
BenchmarkFmtSprintfMore-8         235827             70838 ns/op
BenchmarkFmtSprintfMore-8         403614            102891 ns/op
BenchmarkFmtSprintfMore-8         388071            103072 ns/op
BenchmarkAddMore-8                413247            132776 ns/op
BenchmarkAddMore-8                412111            129470 ns/op
BenchmarkAddMore-8                403748            127128 ns/op
BenchmarkStringsJoinMore-8        404487            113118 ns/op
BenchmarkStringsJoinMore-8        392866            111663 ns/op
BenchmarkStringsJoinMore-8        399028            112117 ns/op
BenchmarkBufferMore-8           77135485                17.27 ns/op
BenchmarkBufferMore-8           87218017                17.58 ns/op
BenchmarkBufferMore-8           85368238                14.25 ns/op
BenchmarkStringBuilderMore-8    92404837                13.56 ns/op
BenchmarkStringBuilderMore-8    94131186                14.14 ns/op
BenchmarkStringBuilderMore-8    92947599                13.59 ns/op
PASS
ok      StudyProject/src/second/test    400.951s

结论

使用strings.Builder的效果最好,当然如果是平时使用少量的拼接笔者还是会使用+。下面我们来看看为啥差距这么大。

背后原因

因为上面的压测结果,前面三种都差不多,所以都归位+,后面的Buffer和StringBuilder都是用到了buffer来优化,但是性能还是有稍许差别,所以下面分为两类,为啥+性能那么差,为啥bytes.Bufferstrings.Builder有稍许区别。

+性能那么差

差的原因在于每次+都要两个string复制到新分配的string中,每次循环都要随着需要拼接后的字符串越长需要重新分配的内存越大,同样需要销毁的空间也越来越大。

为啥bytes.Bufferstrings.Builder有稍许区别

buffer的扩充算法不一样。代码还涉及内建函数,我们可以通过单测来看看扩充的不同:

func TestBuilderConcat(t *testing.T) {
   var str = "1"
   var builder strings.Builder
   cap := 0
   for i := 0; i < 10000; i++ {
      if builder.Cap() != cap {
         fmt.Print(builder.Cap(), " ")
         cap = builder.Cap()
      }
      builder.WriteString(str)
   }
}

func TestBufferConcat(t *testing.T) {

   var str = "1"
   buffer := bytes.Buffer{}
   cap := 0
   for i := 0; i < 10000; i++ {
      if buffer.Cap() != cap {
         fmt.Print(buffer.Cap(), " ")
         cap = buffer.Cap()
      }
      buffer.WriteString(str)
   }
}

输出:

➜  test go test -run="TestBufferConcat" . -v
=== RUN   TestBufferConcat
64 129 259 519 1039 2079 4159 8319 16639 --- PASS: TestBufferConcat (0.00s)
PASS
ok      StudyProject/src/second/test    0.321s
➜  test go test -run="TestBuilderConcat" . -v
=== RUN   TestBuilderConcat
8 16 32 64 128 256 512 896 1408 2048 3072 4096 5376 6912 9472 12288 --- PASS: TestBuilderConcat (0.00s)
PASS
ok      StudyProject/src/second/test    0.235s

bytes.Buffer无脑2n+1,strings.Builder前期会2n到了512之后就不会翻倍扩充。

番外-测试

testing包提供了自动测试的支持。文件名需要以_test.go结尾。测试文件和被测试文件放在一起不会被build,想要build测试文件可以使用“go test”命令。更多的信息,可以执行"go help test" 和 "go help testflag"查看。

基准测试

func BenchmarkXxx(*testing.B)

上面func格式被认为是一个基准测试,可以使用 go test -bench 按顺序执行。

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

基准方法是被执行b.N次。在基准测试执行期间,b.N会被调整,直到基准测试函数持续的时间足够长以至耗时稳定。结果如下:

➜  test go test -bench="."                  
goos: darwin
goarch: arm64
pkg: StudyProject/src/second/test
BenchmarkRandInt-8      85999309                13.48 ns/op
PASS
ok      StudyProject/src/second/test    2.175s

Examples

使用注释提供输出结果和结果进行比较进行测试。(笔者觉得这个有点太随意了,很容易出错。)

有序的

func ExampleHello() {
   fmt.Println("hello")
   fmt.Println("goodbye")
   // Output:
   // hello,and
   // goodbye
}

输出:

➜  test go test -run="ExampleHello" -v
=== RUN   ExampleHello
--- FAIL: ExampleHello (0.00s)
got:
hello
goodbye
want:
hello,and
goodbye
FAIL
exit status 1
FAIL    StudyProject/src/second/test    0.408s

无序的:

func ExamplePerm() {
   for i := 0; i < 5; i++ {
      fmt.Println(i)
   }
   // Unordered output: 4
   // 2
   // 1
   // 3
   // 0
}

输出:

➜  test go test -run="ExamplePerm" -v 
testing: warning: no tests to run
PASS
ok      StudyProject/src/second/test    0.391s
➜  test go test -run="ExamplePerm" -v
=== RUN   ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
ok      StudyProject/src/second/test    0.303s

Fuzzing

Fuzzing是一种自动化的测试技术,它不断的创建输入用来测试程序的bug。Go fuzzing使用覆盖率智能指导遍历被模糊测试的代码,发现缺陷并报告给用户。由于模糊测试可以达到人类经常忽略的边缘场景,因此它对于发现安全漏洞和缺陷特别有价值。

下面是一个模糊测试的示例,突出标识了它的主要组件。

image.png

func FuzzHex(f *testing.F) {
   for _, seed := range [][]byte{{}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
      f.Add(seed)
   }

   f.Fuzz(func(t *testing.T, in []byte) {
      enc := hex.EncodeToString(in)
      out, err := hex.DecodeString(enc)
      if err != nil {
         t.Fatalf("%v: decode: %v", in, err)
      }

      if !bytes.Equal(in, out) {
         t.Fatalf("%v: not equal after round trip: %v", in, out)
      }
   })
}

输出:

➜  test go test -run="FuzzHex" -v           
=== RUN   FuzzHex
=== RUN   FuzzHex/seed#0
=== RUN   FuzzHex/seed#1
=== RUN   FuzzHex/seed#2
=== RUN   FuzzHex/seed#3
=== RUN   FuzzHex/seed#4
--- PASS: FuzzHex (0.00s)
    --- PASS: FuzzHex/seed#0 (0.00s)
    --- PASS: FuzzHex/seed#1 (0.00s)
    --- PASS: FuzzHex/seed#2 (0.00s)
    --- PASS: FuzzHex/seed#3 (0.00s)
    --- PASS: FuzzHex/seed#4 (0.00s)
PASS
ok      StudyProject/src/second/test    0.693s

参考

benchmark 基准测试

一文告诉你神奇的Go内建函数源码在哪里

Benchmarks

[译] go fuzzing

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7126102335415631885