字符串拼接 Benchmark 测试

431 阅读3分钟

字符串拼接操作及性能测试报告

可以遗憾,但不要后悔。 

我们留在这里,从来不是身不由己。 

——— 而是选择在这里经历生活

目录

本文主要围绕 Golang 的字符串拼接操作所展开,介绍了其基本用法和各种方式的基准性能测试报告。

  1. 拼接操作
  2. 基准测试
  3. 高性能方案

拼接操作

💡 在 Go 中,可以使用以下三种方式进行字符串的拼接操作。

使用 + 运算符

可以使用 + 运算符将两个字符串连接起来,如:

str1 := "Hello"
str2 := "world"
result := str1 + " " + str2         // "Hello world"

注意,这种方法可能会导致内存分配问题和效率问题,特别是在循环中进行大量字符串拼接操作时。

使用 strings.Join() 函数

可以使用 strings.Join() 函数将多个字符串拼接起来,如:

strs := []string{"Hello", "world"}
result := strings.Join(strs, " ")   // "Hello world"

这种方法比使用 + 运算符更高效,因为它使用了一个内置的缓冲区来拼接字符串。

使用 bytes.Buffer 类型

可以使用 bytes.Buffer 类型来动态构建一个字符串,如:

var buffer bytes.Buffer
buffer.WriteString("Hello")
buffer.WriteString(" ")
buffer.WriteString("world")
result := buffer.String()           // "Hello world"

这种方法也比使用 + 运算符更高效,因为它使用了一个内置的缓冲区来拼接字符串,并避免了内存分配问题。

基准测试

测试目标

💡 Go 中字符串拼接的操作方式性能对比。

测试方案

在测试函数中,我们使用 testing.B 类型的 N 字段来指定基准测试的次数,这里使用 b.N 表示让 Go 运行基准测试足够长的时间(通常为 10s),并根据实际情况动态调整基准测试次数,以获取更准确的测试结果。

序号描述方式代码复杂度
方式1+ 运算符+简单,友好
方式2Join 方法strings.Join()适中,还凑活
方式3Buffer 缓冲bytes.Buffer.WriteString()繁琐,不太友好

测试运行

  • 代码
package test

import (
   "bytes"
   "testing"
)

// BenchmarkStringJoinWithPlus 测试使用 "+" 运算符拼接字符串的性能
func BenchmarkStringJoinWithPlus(b *testing.B) {
    str1 := "Hello"
    str2 := "world"
    for i := 0; i < b.N; i++ {
        result := str1 + " " + str2
        _ = result                     // 避免编译器优化代码
    }
}

// BenchmarkStringJoinWithBuffer 测试使用 bytes.Buffer 拼接字符串的性能
func BenchmarkStringJoinWithBuffer(b *testing.B) {
    str1 := "Hello"
    str2 := "world"
    for i := 0; i < b.N; i++ {
        var buffer bytes.Buffer
        buffer.WriteString(str1)
        buffer.WriteString(" ")
        buffer.WriteString(str2)
        result := buffer.String()
        _ = result                     // 避免编译器优化代码
    }
}
  • 命令
$ go test -bench=. -benchmem

其中,-bench=. 表示运行所有的基准测试函数,-benchmem 表示在测试结果中包含内存分配的信息。

测试结果

  • 2 个字符串拼接
方法10s 内执行次数执行效率排名
+45265197325.04 ns/op中等
strings.Join()61030250220.06 ns/op最高
bytes.Buffer.WriteString()32892341436.97 ns/op最低
  • 3 个字符串拼接
方法10s 内执行次数执行效率排名
+26091025646.60 ns/op最低
strings.Join()44620745126.79 ns/op最高
bytes.Buffer.WriteString()28780127541.04 ns/op中等
  • 4 个字符串拼接
方法10s 内执行次数执行效率排名
+17198334069.46 ns/op最低
strings.Join()35958372233.38 ns/op最高
bytes.Buffer.WriteString()25720612246.97 ns/op中等
  • 5 个字符串拼接
方法10s 内执行次数执行效率排名
+14396213083.85 ns/op最低
strings.Join()29906146340.68 ns/op最高
bytes.Buffer.WriteString()23421829951.32 ns/op中等
  • 10 个字符串拼接
方法10s 内执行次数执行效率排名
+60857520183.7 ns/op最低
strings.Join()16006283275.31 ns/op中等
bytes.Buffer.WriteString()17029556872.36 ns/op最高
  • 15 个字符串拼接
方法10s 内执行次数执行效率排名
+37362598317.6 ns/op最低
strings.Join()100000000109.5 ns/op中等
bytes.Buffer.WriteString()12679335296.11 ns/op最高
  • 30 个字符串拼接
方法10s 内执行次数执行效率排名
+16936318676.7 ns/op最低
strings.Join()52884830222.3 ns/op中等
bytes.Buffer.WriteString()75862920155.0 ns/op最高
  • 50 个字符串拼接
方法10s 内执行次数执行效率排名
+105474501146 ns/op最低
strings.Join()32108361377.0 ns/op中等
bytes.Buffer.WriteString()51436773236.8 ns/op最高
  • 100 个字符串拼接
方法10s 内执行次数执行效率排名
+48278762454 ns/op最低
strings.Join()16298336752.6 ns/op中等
bytes.Buffer.WriteString()25483066473.2 ns/op最高

测试结论

💡 不要有思维定势,技术说到底,如何取舍,两 "害" 取其轻。

常规项目使用建议:

  • 2 个或最多 3 个字符串拼接时,使用 + 号,操作简单,虽然性能差,可舍弃一些极致的性能。
  • 当超过 2 个以上,< 50 个字符串拼接时,建议使用 strings.Join() 方法,性能很高,操作同样方便。
  • 虽然性能最高,通常不建议,操作相对繁琐,除非 > 50 个字符串的拼接,buffer 方式性能优势明显。

如果项目性能要求非常高,那自行判断取舍吧。

高性能方案

💡 用 Go 还是要学着适应自己造轮子,可以使用 strings.Builder 类型来拼接字符串,它是 Go 1.10 引入的类型,专门用于高效地构建字符串。

一个简单的封装

package test

import (
    "strings"
    "testing"
)

// 可接受任意多个字符串拼接
func JoinStrings(strs ...string) string {
    var builder strings.Builder
    for _, s := range strs {
        builder.WriteString(s)
    }
    return builder.String()
}

func TestJoinString(t *testing.T) {
    result := utils.JoinStrings("Hello", " ", "world")
    fmt.Println(result)
}

终极字符串化器

package test

import (
    "encoding/json"
    "fmt"
    "reflect"
    "strconv"
    "strings"
    "testing"
)

// 可接受任意多个覆盖大部分类型的转换为字符串的终极拼接
func UltimateStringifier(values ...interface{}) string {
    var builder strings.Builder
    for _, v := range values {
        switch v := v.(type) {
        case string:
            builder.WriteString(v)
        case int:
            builder.WriteString(strconv.Itoa(v))
        case byte:
            builder.WriteByte(v)
        case rune:
            builder.WriteRune(v)
        case bool:
            builder.WriteString(strconv.FormatBool(v))
        case float32:
            builder.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
        case float64:
            builder.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
        case []string:
            for _, s := range v {
                builder.WriteString(s)
            }
        case []int:
            for _, i := range v {
                builder.WriteString(strconv.Itoa(i))
            }
        case []byte:
            builder.Write(v)
        default:
            // check if v implements the fmt.Stringer interface
            if str, ok := v.(fmt.Stringer); ok {
                builder.WriteString(str.String())
            } else {
                // try to marshal v into JSON
                b, err := json.Marshal(v)
                if err == nil {
                    builder.Write(b)
                } else {
                    // fallback to reflect.Value.String()
                    rv := reflect.ValueOf(v)
                    if rv.Kind() == reflect.Ptr && rv.IsNil() {
                        builder.WriteString("<nil>")
                    } else {
                        builder.WriteString(rv.String())
                    }
                }
            }
        }
    }
    return builder.String()
}

func TestUltimateConcatenatedString(t *testing.T) {
    v1 := []string{"Hello", " ", "world", "-"}
    v2 := []byte{'A', 'B', 'C'}
    v3 := []int{1, 2, 3}
    v4 := '😊'
    v5 := 6.66
    v6 := false
    v7 := 10000
    v8 := "ohh~"
    v9 := map[string]interface{}{"k1": 1, "k2": "d", "k3": true}
    v10 := (*interface{})(nil)
    result := UltimateStringifier(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)
    fmt.Println(result)
}