找回密码
 会员注册
查看: 24|回复: 0

Golang高性能编程实践

[复制链接]

5

主题

0

回帖

16

积分

新手上路

积分
16
发表于 2024-9-20 17:00:16 | 显示全部楼层 |阅读模式
作者:colygo中高性能编程是一个经久不衰的话题,本文尝试从实践及源码层面对go的高性能编程进行解析。1.为什么要进行性能优化服务上线前,为什么要进行压测和性能的优化?一个例子,content-service在压测的时候发现过一个问题:旧逻辑为了简化编码,在进行协议转换前,会对某些字段做一个DeepCopy,因为转换过程需要原始数据,但我们完全可以通过一些处理逻辑的调整,比如调整先后顺序等移除DeepCopy。优化前后性能对比如下:阶段AVG(ms)P95(ms)P99(ms)CPU/MEM优化前67.96153.59212.85100%/34%优化后9.1223.2238.9884%/34%性能有7倍左右提升,改动很小,但折算到成本上的收益是巨大的。在性能优化上任何微小的投入,都可能会带来巨大的收益那么,如何对go程序的性能进行度量和分析?2.度量和分析工具2.1Benchmark2.1.1Benchmark示例func BenchmarkConvertReflect(b *testing.B) {    var v interface{} = int32(64)    for i:=0;ireflect没有逃逸的原因参见:iface.gocontent-service中已经不再使用reflect相关的转换处理3.2常用mapgo中常用的map包含,runtime.map、sync.map和第三方的ConcurrentMap,go中map的定义位于map.go,典型的基于bucket的map的实现,如下:type hmap struct {    ......    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)    hash0     uint32 // hash seed    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing  ......}其查找、删除、rehash机制参见https://juejin.cn/post/7056290831182856205sync.map定义位于map.go中,其是典型的以空间换时间的处理,具体如下:type readOnly struct {    m       map[interface{}]*entry    amended bool // true if the dirty map contains some key not in m.}type entry struct {    p unsafe.Pointer // *interface{}}type Map struct {    mu Mutex    read atomic.Value // readOnly数据    dirty map[interface{}]*entry    misses int}read中存储的是dirty数据的一个副本(通过指针),在读多写少的情况下,基本可以实现无锁的数据读取。Sync.map相关机制参见:https://juejin.cn/post/6844903895227957262go中还有一个第三方的ConcurrentMap,其采用分段锁的原理,通过降低锁的粒度提升性能,参见:current-map针对map、sync.map、ConcurrentMap的测试如下:const mapCnt = 20func BenchmarkStdMapGetSet(b *testing.B) {    mp := map[string]string{}    keys := []string{"a", "b", "c", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r"}    for i := range keys {        mp[keys[i]] = keys[i]    }    var m sync.Mutex    b.ResetTimer()    b.RunParallel(func(pb *testing.PB) {        for pb.Next() {            for i := 0; i 32时,每次会进行空间的分配和拷贝处理,其处理如下:func concatstrings(buf *tmpBuf, a []string) string {    idx := 0    l := 0    count := 0    for i, x := range a {  // 计算+链接字符的长度        n := len(x)        if n == 0 {            continue        }        if l+n 8B):其中,直接从p.mcache获取空间不需要加锁(单协程),mheap.mcentral获取空间需要加锁(全局变量)、mmap需要系统调用。此外,堆上分配还需要考虑gc导致的stw等的影响,因此建议所需空间不是特别大时还是在栈上进行空间的分配。content-service开发中有一个共识:能在栈上处理的数据,不会放到堆上。4.2ZeroGCZeroGC能够避免gc带来的扫描、STW等,具有一定的性能收益。当前zerogc的处理,主要包含2种:无gc,通过mmap或者cgo.malloc分配空间,绕过go的内存分配机制,如fastcache的实现避免或者减少gc,通过[]byte等避免因为指针导致的扫描、stw,bigCache的实现即为此。ZeroGC的优点在于,避免了gogc处理带来的标记扫描、STW等,相对于常规堆上数据分配,其性能有较大提升。content-service在重构中,使用了大量的基于0gc的库,比如fastcache,对一些常用函数、机制,如strings.split也进行了0gc的优化,其实现如下:在content-service中其实现位于string_util.go,如下:type StringSplitter struct {    Idx [8]int  // 存储splitter对应的位置信息    src string    cnt int}// Split 分割func (s *StringSplitter) Split(str string, sep byte) bool {    s.src = str    for i := 0; i = len(s.Idx) {                return false            }        }    }    return true}与常规strings.split对比如下,其性能有近4倍左右提升:➜  test go test --bench='Split' -run=none -benchmemgoos: darwingoarch: amd64pkg: gotest666/testcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkQSplitRaw-12       13455728            76.43 ns/op       64 B/op          1 allocs/opBenchmarkQSplit-12          59633916            20.08 ns/op        0 B/op          0 allocs/opPASS4.3GC的优化gc优化相关,主要涉及GOGC、GOMEMLIMIT,参见:Golang垃圾回收介绍及参数调整需要注意,此机制只在1.20以上版本生效4.4逃逸对于一些处理比较复杂操作,go在编译器会在编译期间将相关变量逃逸至堆上。目前可能导致逃逸的机制包含:基于指针的逃逸栈空间不足,超过了os的限制8M闭包动态类型目前逃逸分析,可采用-gcflags=-m进行查看,如下:type test1 struct {    a int32    b int    c int32}type test2 struct {    a int32    c int32    b int}func getData() *int {    a := 10    return &a}func main() {    fmt.Println(unsafe.Sizeof(test1{}))    fmt.Println(unsafe.Sizeof(test2{}))    getData()}➜  gotest666 go build -gcflags=-m main.go# command-line-arguments./main.go:20:6: can inline getData./main.go:26:13: inlining call to fmt.Println./main.go:27:13: inlining call to fmt.Println./main.go:28:9: inlining call to getData./main.go:21:2: moved to heap: a        // 返回指针导致逃逸./main.go:26:13: ... argument does not escape./main.go:26:27: unsafe.Sizeof(test1{}) escapes to heap // 动态类型导致逃逸./main.go:27:13: ... argument does not escape./main.go:27:27: unsafe.Sizeof(test2{}) escapes to heap // 动态类型导致逃逸在日常业务处理过程中,建议尽量避免逃逸到堆上的情况4.5数据的对齐go中同样存在数据对齐,适当的布局调整,能够节省大量的空间,具体如下:type test1 struct {    a int32    b int    c int32}type test2 struct {    a int32    c int32    b int}func main() {    fmt.Println(unsafe.Alignof(test1{}))    fmt.Println(unsafe.Alignof(test2{}))    fmt.Println(unsafe.Sizeof(test1{}))    fmt.Println(unsafe.Sizeof(test2{}))}➜  gotest666 go run main.go8824164.6空间预分配空间预分配,可以避免大量不必要的空间分配、拷贝,目前slice、map、strings.Builder、byte.Builder等都涉及到预分配机制。以map为例,测试结果如下:func BenchmarkConcurrentMapAlloc(b *testing.B) {    m := map[int]int{}    b.ResetTimer()    for i := 0; i 
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 00:55 , Processed in 0.855275 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表