Go高性能

2年前 (2022) 程序员胖胖胖虎阿
230 0 0

Benchmark(基准测评)

参考文章:https://blog.logrocket.com/be...

字符串拼接性能及其原理

Go官方中推荐使用StringBuffer来拼接字符串
在Go语言中,字符串是不可变的,拼接字符串事实上是创建了一个新的字符串变量。可想而知,如果大量使用"+"运算符,则程序的性能将大打折扣。string底层使用了byte数组...

常见的拼接方式

https://geektutu.com/post/hpg...
1 使用 +

func plusConcat(n int, str string) string {
    s := ""
    for i := 0; i < n; i++ {
        s += str
    }
    return s
}

2 使用 fmt.Sprintf

func sprintfConcat(n int, str string) string {
    s := ""
    for i := 0; i < n; i++ {
        s = fmt.Sprintf("%s%s", s, str)
    }
    return s
}

3 使用 bytes.Buffer

func bufferConcat(n int, s string) string {
    buf := new(bytes.Buffer)
    for i := 0; i < n; i++ {
        buf.WriteString(s)
    }
    return buf.String()
}

4 使用 []byte

func byteConcat(n int, str string) string {
    buf := make([]byte, 0)
    for i := 0; i < n; i++ {
        buf = append(buf, str...)
    }
    return string(buf)
}

5 使用 strings.Builder

func builderConcat(n int, str string) string {
    var builder strings.Builder
    for i := 0; i < n; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

如果长度是可预知的,那么创建 []byte 时,我们还可以预分配切片的容量(cap)。

func preByteConcat(n int, str string) string {
    buf := make([]byte, 0, n*len(str))
    for i := 0; i < n; i++ {
        buf = append(buf, str...)
    }
    return string(buf)
}

benchmark 性能比拼

每个 benchmark 用例中,生成了一个长度为 10 的字符串,并拼接 1w 次。

func benchmark(b *testing.B, f func(int, string) string) {
    var str = randomString(10)
    for i := 0; i < b.N; i++ {
        f(10000, str)
    }
}

func BenchmarkPlusConcat(b *testing.B)    { benchmark(b, plusConcat) }
func BenchmarkSprintfConcat(b *testing.B) { benchmark(b, sprintfConcat) }
func BenchmarkBuilderConcat(b *testing.B) { benchmark(b, builderConcat) }
func BenchmarkBufferConcat(b *testing.B)  { benchmark(b, bufferConcat) }
func BenchmarkByteConcat(b *testing.B)    { benchmark(b, byteConcat) }
func BenchmarkPreByteConcat(b *testing.B) { benchmark(b, preByteConcat) }

运行该用例:

$ go test -bench="Concat$" -benchmem .
goos: darwin
goarch: amd64
pkg: example
BenchmarkPlusConcat-8         19      56 ms/op   530 MB/op   10026 allocs/op
BenchmarkSprintfConcat-8      10     112 ms/op   835 MB/op   37435 allocs/op
BenchmarkBuilderConcat-8    8901    0.13 ms/op   0.5 MB/op      23 allocs/op
BenchmarkBufferConcat-8     8130    0.14 ms/op   0.4 MB/op      13 allocs/op
BenchmarkByteConcat-8       8984    0.12 ms/op   0.6 MB/op      24 allocs/op
BenchmarkPreByteConcat-8   17379    0.07 ms/op   0.2 MB/op       2 allocs/op
PASS
ok      example 8.627s

从基准测试的结果来看,使用 + 和 fmt.Sprintf 的效率是最低的,和其余的方式相比,性能相差约 1000 倍,而且消耗了超过 1000 倍的内存。当然 fmt.Sprintf 通常是用来格式化字符串的,一般不会用来拼接字符串。

strings.Builder、bytes.Buffer 和 []byte 的性能差距不大,而且消耗的内存也十分接近,性能最好且消耗内存最小的是 preByteConcat,这种方式预分配了内存,在字符串拼接的过程中,不需要进行字符串的拷贝,也不需要分配新的内存,因此性能最好,且内存消耗最小。

Go实现集合(Set)

运用了空结构体的特性

Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。事实上,对于集合来说,只需要 map 的键,而不需要值。即使是将值设置为 bool 类型,也会多占据 1 个字节,那假设 map 中有一百万条数据,就会浪费 1MB 的空间。

因此呢,将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。

type Set map[string]struct{}

func (s Set) Has(key string) bool {
    _, ok := s[key]
    return ok
}

func (s Set) Add(key string) {
    s[key] = struct{}{}
}

func (s Set) Delete(key string) {
    delete(s, key)
}

func main() {
    s := make(Set)
    s.Add("Tom")
    s.Add("Sam")
    fmt.Println(s.Has("Tom"))
    fmt.Println(s.Has("Jack"))
}
版权声明:程序员胖胖胖虎阿 发表于 2022年11月6日 上午12:16。
转载请注明:Go高性能 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...