|
作者: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
|
|