本文旨在记录工作、学习过程中遇到的性能优化技巧,会不停的添加内容

优秀文章

常规手段

1.sync.Pool

临时对象池应该是对可读性影响最小且优化效果显著的手段。基本上,业内以高性能著称的开源库,都会使用到。

最典型的就是fasthttp了,它几乎把所有的对象都用sync.Pool维护。但这样的复用不一定全是合理的。比如在fasthttp中,传递上下文相关信息的RequestCtx就是用sync.Pool维护的,这就导致了你不能把它传递给其他的goroutine。如果要在fasthttp中实现类似接受请求->异步处理的逻辑,必须得拷贝一份RequestCtx再传递。这对不熟悉fasthttp原理的使用者来讲,很容易就踩坑了。

还有一种利用sync.Pool特性,来减少锁竞争的优化手段,也非常巧妙,有些在优化随机数的文章有讲【待补充】。另外,在优化前要善用go逃逸检查分析对象是否逃逸到堆上,防止负优化。

2.string2bytes & bytes2string

这也是两个比较常规的优化手段,核心还是复用对象,减少内存分配。在 go 标准库中也有类似的用法gostringnocopy,要注意string2bytes后,不能对其修改。

unsafe.Pointer经常出现在各种优化方案中,使用时要非常小心。这类操作引发的异常,通常是不能recover的。

3.协程池

绝大部分应用场景,go 是不需要协程池的。当然,协程池还是有一些自己的优势:

  1. 可以限制goroutine数量,避免无限制的增长。
  2. 减少栈扩容的次数。
  3. 频繁创建goroutine的场景下,资源复用,节省内存。(需要一定规模。一般场景下,效果不太明显)

go 对goroutine有一定的复用能力。所以要根据场景选择是否使用协程池,不恰当的场景不仅得不到收益,反而增加系统复杂性。

4.反射

go 里面的反射代码可读性本来就差,常见的优化手段进一步牺牲可读性。而且后续马上就有泛型的支持,所以若非必要,建议不要优化反射部分的代码

比较常见的优化手段有:

缓存反射结果,减少不必要的反射次数。例如json-iterator
直接使用unsafe.Pointer根据各个字段偏移赋值
消除一般的struct反射内存消耗go-reflect
避免一些类型转换,如interface->[]byte。可以参考zerolog

5.减小锁消耗

并发场景下,对临界区加锁比较常见。带来的性能隐患也必须重视。常见的优化手段有:

6.字符串操作规避反射

参考zap的设计,尽可能规避反射操作,如果需要进行类型转换 使用strconv的操作,性能会更优异

7.参数逃逸加大GC负担

指针必然逃逸的情况(go 1.13.4 darwin/amd64)

  • 在某个函数中new或者字面量创建出的变量,将其指针作为函数返回值,则该变量逃逸(构造函数返回的指针变量必然逃逸)
  • 被已经逃逸的变量引用的指针,发送逃逸
  • 被指针类型的silce、map和chan引用的指针,发送逃逸

指针必然不逃逸的情况

  • 指针被未发生逃逸的变量引用
  • 仅仅在函数内对变量做取址操作,未将指针传出

可能逃逸情况

  • 将指针作为入参传给别的函数;这里还是要看指针在被传入的函数中的处理过程,如果发生了上边三种情况,则也会逃逸

8.避免使用MapKeys获取map key值

9.复杂迭代 for 循环效率 > range 效率

主要是值拷贝的耗时情况

10.使用 []byte 当做 map 的 key

使用string作为 map 的 key 是很常见的,但有时你拿到的是一个[]byte。
编译器为这种情况实现特定的优化,编译器会避免将字节切片转换为字符串到map查找

var m map[string]string
v, ok := m[string(bytes)]

但,如果你这样写,编译器就不会优化

key := string(bytes)
val, ok := m[key]

11.数组复制 使用copy取代原数组操作

12.使用流式 IO 接口

尽可能避免将数据读入[]byte并传递使用它。

根据请求的不同,你可能会将兆字节(或更多)的数据读入内存。这会给GC带来巨大的压力,并且会增加应用程序的平均延迟。

这种情况最好使用io.Reader和io.Writer构建数据处理流,以限制每个请求使用的内存量。

如果你使用了大量的io.Copy,那么为了提高效率,可以考虑实现io.ReaderFrom/io.WriterTo。 这些接口效率更高,并避免将内存复制到临时缓冲区。

13.超时,超时,还是超时

永远不要在不知道需要多长时间才能完成的情况下执行 IO 操作。

你要在使用SetDeadline,SetReadDeadline,SetWriteDeadline进行的每个网络请求上设置超时。

您要限制所使用的阻塞IO的数量。 使用 goroutine 池或带缓冲的 channel 作为信号量。

var semaphore = make(chan struct{}, 10)

func processRequest(work *Work) {
        semaphore <- struct{}{} // 持有信号量
        // 执行请求
        <-semaphore // 释放信号量
}

1. golink

**golink**在官方的文档里有介绍,使用格式:

//go:linkname FastRand runtime.fastrand
func FastRand() uint32
FastRandruntime.fastrand
runtimemathgoroutineruntime
Benchmark_MathRand-12       84419976            13.98 ns/op
Benchmark_Runtime-12        505765551           2.158 ns/op
time.Now()runtime.walltime1runtime.nanotimeruntime.walltime1
Benchmark_Time-12       16323418            73.30 ns/op
Benchmark_Runtime-12    29912856            38.10 ns/op
runtime.nanotimetime.Now
//go:linkname nanotime1 runtime.nanotime1
func nanotime1() int64
func main() {
    defer func( begin int64) {
        cost := (nanotime1() - begin)/1000/1000
        fmt.Printf("cost = %dms \n" ,cost)
    }(nanotime1())

    time.Sleep(time.Second)
}

运行结果:cost = 1000ms
runtimeg0time.Nowgo<=1.16
g0
g0GMP
未导出方法未导出变量

还有一些其他奇奇怪怪的用法:

reflect.typelinksstructpanichookpanicpanicruntime.main_inittaskinitinitinitGODEBUG=inittracing=1initruntime.asmcgocallcgocgogoroutinecgo

2. log-函数名称行号的获取

虽然很多高性能的日志库,默认都不开启记录行号。但实际业务场景中,我们还是觉得能打印最好。

在**runtime**中,函数行号和函数名称的获取分为两步:

runtimegoroutinefuncInfo
runtimepc
var(
    m sync.Map
)
func Caller(skip int)(pc uintptr, file string, line int, ok bool){
    rpc := [1]uintptr{}
    n := runtime.Callers(skip+1, rpc[:])
    if n < 1 {
        return
    }
    var (
        frame  runtime.Frame
        )
    pc  = rpc[0]
    if item,ok:=m.Load(pc);ok{
        frame = item.(runtime.Frame)
    }else{
        tmprpc := []uintptr{
            pc,
        }
        frame, _ = runtime.CallersFrames(tmprpc).Next()
        m.Store(pc,frame)
    }
    return frame.PC,frame.File,frame.Line,frame.PC!=0
}

压测数据如下,优化后稍微减轻这部分的负担,同时消除掉不必要的内存分配。

BenchmarkCaller-8       2765967        431.7 ns/op         0 B/op          0 allocs/op
BenchmarkRuntime-8      1000000       1085 ns/op         216 B/op          2 allocs/op

3.cgo

cgoc++ccgocgocgog0mruntimemcgo
cgo
go tool cgo main.go
cgoruntime.cgocallruntime.cgocall
incgomg0c
casmcgocall
package main

/*
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
struct args{
    int p1,p2;
    int r;
};
int add(struct args* arg) {
    arg->r= arg->p1 + arg->p2;
    return 100;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)
//go:linkname asmcgocall runtime.asmcgocall
func asmcgocall(unsafe.Pointer, uintptr) int32

func main() {
    arg := C.struct_args{}
    arg.p1 = 100
    arg.p2 = 200
    //C.add(&arg)
    asmcgocall(C.add,uintptr(unsafe.Pointer(&arg)))
    fmt.Println(arg.r)
}

压测数据如下:

BenchmarkCgo-12             16143393    73.01 ns/op     16 B/op        1 allocs/op

BenchmarkAsmCgoCall-12      119081407   9.505 ns/op     0 B/op         0 allocs/op

4.epoll

runtimeruntime/netpool
readwritenetpoolepoll
x/unix

在我们的项目中,也有尝试过使用。最终我们还是觉得基于标准库的实现已经足够。理由如下:

goroutinenetpoolnetpoolepoll

5.包大小优化

tlinux2.2 golang1.15size —Asectiondebugsection sizesection
size -A test-30MB
section                  size       addr
.interp                    28    4194928
.note.ABI-tag              32    4194956
... ... ... ...
.zdebug_aranges          1565          0
.zdebug_pubnames        56185          0
.zdebug_info          2506085          0
.zdebug_abbrev          13448          0
.zdebug_line          1250753          0
.zdebug_frame          298110          0
.zdebug_str             40806          0
.zdebug_loc           1199790          0
.zdebug_pubtypes       151567          0
.zdebug_ranges         371590          0
.debug_gdb_scripts         42          0
Total                93653020

size -A test-50MB
section                   size       addr
.interp                     28    4194928
.note.ABI-tag               32    4194956
.note.go.buildid           100    4194988
... ... ...
.debug_aranges            6272          0
.debug_pubnames         289151          0
.debug_info            8527395          0
.debug_abbrev            73457          0
.debug_line            4329334          0
.debug_frame           1235304          0
.debug_str              336499          0
.debug_loc             8018952          0
.debug_pubtypes        1072157          0
.debug_ranges          2256576          0
.debug_gdb_scripts          62          0
Total                113920274
debugzdebugzdebugdebugzipdebugzip
debugcgoc++
cgog++ldcgo
ld--compress-debug-sections=zlib-gnudebug
tlinux2.2go 1.16go1.16ldyum install -y binutilsldldtlinux2.2ld--compress-debug-sections=zlib-gnuld
cgold2.27

6.simd

simdsimd
llvmcgocgocgo
llvm
csimdllvm.s.s

以下开源库用到了 simd,可以参考:

simd
  1. 难以维护,要么需要懂汇编的大神,要么需要引入第三方语言
  2. 跨平台支持不够,需要对不同平台汇编指令做适配
  3. 汇编代码很难调试,作为使用方来讲,完全黑盒

7.jit

go 中使用 jit 的方式可以参考**Writing a JIT compiler in Golang**

json

这种使用方式个人感觉在 go 中意义不大,仅供参考

总结

过早的优化是万恶之源,千万不要为了优化而优化

  1. pprof 分析,竞态分析,逃逸分析,这些基础的手段是必须要学会的
  2. 常规的优化技巧是比较实用的,他们往往能解决大部分的性能问题并且足够安全。
  3. 在一些着重性能的基础库中,使用一些非常规的优化手段也是可以的,但必须要权衡利弊,不要过早放弃可读性,兼容性和稳定性。

参考:

  1. https://zhuanlan.zhihu.com/p/403417640
  2. https://github.com/geektutu/high-performance-go
  3. https://mp.weixin.qq.com/s/i0bMh_gLLrdnhAEWlF-xDw
  4. https://github.com/sxs2473/go-performane-tuning/blob/master/5.%E6%8A%80%E5%B7%A7/%E6%8A%80%E5%B7%A7.md
  5. https://www.jianshu.com/p/662c8f8e5740