常规手段
1.sync.Pool
sync.PoolfasthttpRequestCtxsync.PoolgoroutinefasthttpRequestCtxfasthttp
go逃逸检查
2.string2bytes & bytes2string
这也是两个比较常规的优化手段,核心还是复用对象,减少内存分配。
在go标准库中也有类似的用法gostringnocopy
string2bytes
unsafe.Pointerrecover
3.协程池
绝大部分应用场景,go是不需要协程池的。当然,协程池还是有一些自己的优势:
goroutinegoroutine
goroutine
4.反射
go里面的反射代码可读性本来就差,常见的优化手段进一步牺牲可读性。
而且后续马上就有范型的支持,所以若非必要,建议不要优化反射部分的代码
比较常见的优化手段有:
unsafe.Pointerstructinterface->[]byte
5.减小锁消耗
并发场景下,对临界区加锁比较常见。带来的性能隐患也必须重视。常见的优化手段有:
另类手段
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
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
6.simd
simdsimd
llvmcgocgocgo
llvm
csimdllvm.s.s
以下开源库用到了simd,可以参考:
simd
- 难以维护,要么需要懂汇编的大神,要么需要引入第三方语言
- 跨平台支持不够,需要对不同平台汇编指令做适配
- 汇编代码很难调试,作为使用方来讲,完全黑盒
7.jit
go中使用jit的方式可以参考Writing a JIT compiler in Golang
json
这种使用方式个人感觉在go中意义不大,仅供参考
总结
过早的优化是万恶之源,千万不要为了优化而优化
- pprof分析,竞态分析,逃逸分析,这些基础的手段是必须要学会的
- 常规的优化技巧是比较实用的,他们往往能解决大部分的性能问题并且足够安全。
- 在一些着重性能的基础库中,使用一些非常规的优化手段也是可以的,但必须要权衡利弊,不要过早放弃可读性,兼容性和稳定性。