【Go语言测评】七种字符串拼接方式,哪种最高效?

490 阅读1分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

七种拼接方式

+ 加号拼接

go语言字符串类型是不可编辑的,加号直接拼接相当于创建一个新的字符串,赋值为拼接后的值。以拼接数字1到n为例:

func BenchmarkStringAdd(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      var s string
      for i := 0; i < numbers; i++ {
         s += strconv.Itoa(i)
      }

   }
   b.StopTimer()
}

fmt.Sprintf

fmt.Sprintf是fmt包提供的字符串生成函数,可以将约定格式的字符串直接返回。

func BenchmarkSprintf(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      var s string
      for i := 0; i < numbers; i++ {
         s = fmt.Sprintf("%v%v", s, i)
      }
   }
   b.StopTimer()
}

string Buffer

string Buffer底层用byte切片存储字符串,可以动态扩展

func BenchmarkBytesBuf(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      var buf bytes.Buffer
      for i := 0; i < numbers; i++ {
         buf.WriteString(strconv.Itoa(i))
      }
      _ = buf.String()
   }
   b.StopTimer()
}

string Builder

string Builder与string Buffer类型,但是toString方法可以原地返回字符串,无需开辟新空间。

func BenchmarkStringBuilder(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      var builder strings.Builder
      for i := 0; i < numbers; i++ {
         builder.WriteString(strconv.Itoa(i))
      }
      _ = builder.String()
   }
   b.StopTimer()
}

[]byte

byte切片可以append进行字符拼接,最后返回string即可。

func BenchmarkBytes(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      bytes := make([]byte, 0)
      for i := 0; i < numbers; i++ {
         bytes = append(bytes, strconv.Itoa(i)...)
      }
      _ = string(bytes)
   }
   b.StopTimer()
}

[]rune

rune切片与byte切片类似,不过rune占3个字节相当于一个uint,1个rune可以存储一个汉字。

func BenchmarkRunes(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      runes := make([]rune, 0)
      for i := 0; i < numbers; i++ {
         runes = append(runes, []rune(strconv.Itoa(i))...)
      }
      _ = string(runes)
   }
   b.StopTimer()
}

预分配空间的切片

append主要的开销是切片长度不够,重新分配空间。别小看这点常数级优化。

func BenchmarkStringBytes2(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      bytes := make([]byte, 0, 200)
      for i := 0; i < numbers; i++ {
         bytes = append(bytes, strconv.Itoa(i)...)
      }
      _ = string(bytes)

   }
   b.StopTimer()
}

func BenchmarkRunes2(b *testing.B) {
   b.ResetTimer()
   for idx := 0; idx < b.N; idx++ {
      runes := make([]rune, 0, 200)
      for i := 0; i < numbers; i++ {
         runes = append(runes, []rune(strconv.Itoa(i))...)
      }
      _ = string(runes)
   }
   b.StopTimer()
}

性能测试

拼接1~100 读取1次

goarch: amd64
pkg: learn/basic/prof/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkStringAdd-16             272563              4254 ns/op
BenchmarkSprintf-16               107931             10988 ns/op
BenchmarkBytesBuf-16             1616385               749.9 ns/op
BenchmarkStringBuilder-16        1935043               622.3 ns/op
BenchmarkBytes-16                1818974               689.7 ns/op
BenchmarkRunes-16                 479460              2465 ns/op
BenchmarkStringBytes2-16         2806760               433.3 ns/op
BenchmarkRunes2-16                647060              1841 ns/op

拼接1~100 读100次

goos: windows
goarch: amd64
pkg: learn/basic/prof/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics         
BenchmarkStringAdd-16             289051              4169 ns/op
BenchmarkSprintf-16               109444             11122 ns/op
BenchmarkBytesBuf-16              376160              3263 ns/op
BenchmarkStringBuilder-16        1845380               643.5 ns/op
BenchmarkBytes-16                 380598              3168 ns/op
BenchmarkRunes-16                  25981             45550 ns/op
BenchmarkStringBytes2-16          444042              2889 ns/op
BenchmarkRunes2-16                 26660             44863 ns/op

拼接1~10000 读取1次

goos: windows
goarch: amd64
pkg: learn/basic/prof/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkStringAdd-16                 56          22105196 ns/op
BenchmarkSprintf-16                   45          26228936 ns/op
BenchmarkBytesBuf-16                5314            216823 ns/op
BenchmarkStringBuilder-16           5709            206068 ns/op
BenchmarkBytes-16                   5842            208099 ns/op
BenchmarkRunes-16                   1687            631374 ns/op
BenchmarkStringBytes2-16            6655            191255 ns/op
BenchmarkRunes2-16                  2181            548359 ns/op

拼接1~10000 读10000次

goos: windows
goarch: amd64
pkg: learn/basic/prof/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics         
BenchmarkStringAdd-16                 52          21811823 ns/op
BenchmarkSprintf-16                   45          26745293 ns/op
BenchmarkBytesBuf-16                  54          23539459 ns/op
BenchmarkStringBuilder-16           6030            205297 ns/op
BenchmarkBytes-16                     50          25721030 ns/op
BenchmarkRunes-16                      2         862442350 ns/op
BenchmarkStringBytes2-16              50          26813260 ns/op
BenchmarkRunes2-16                     2         867332600 ns/op

拼接1~1000000 读取1次

goos: windows
goarch: amd64
pkg: learn/basic/prof/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkStringAdd-16                 0           bomm ns/op
BenchmarkSprintf-16                   0           bomm ns/op
BenchmarkBytesBuf-16                  45          26077220 ns/op
BenchmarkStringBuilder-16             45          27934771 ns/op
BenchmarkBytes-16                     43          28071884 ns/op
BenchmarkRunes-16                     15          75816607 ns/op
BenchmarkStringBytes2-16              54          22541828 ns/op
BenchmarkRunes2-16                    16          64386250 ns/op

拼接1~1000000 读取1000000次

goos: windows
goarch: amd64
pkg: learn/basic/prof/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkStringAdd-16                 0           bomm ns/op
BenchmarkSprintf-16                   0           bomm ns/op
BenchmarkBytesBuf-16                  0           bomm ns/op
BenchmarkStringBuilder-16             44          27153045 ns/op
BenchmarkBytes-16                     0           bomm ns/op
BenchmarkRunes-16                     0           bomm ns/op
BenchmarkStringBytes2-16              0           bomm ns/op
BenchmarkRunes2-16                    0           bomm ns/op

总结

大体上拼接性能: 在写多读少的场景下: 预分配空间[]byte>string builder ≈ string buffer>[]byte>预分配空间[]rune>[]rune> + >fmt

在写多读多的场景下: string builder >> 其他

这里总结优化思路:

  1. 字符串修改时避免重新分配空间:使用byte切片,尽量预分配大概的空间。
  2. 获取string时,避免byte切片转化为string重新分配空间