fasthttp中很多对象都通过使用sync.Pool达到复用的目的,一是减少内存分配,二是减少GC。复用的对象包括:
我写了一个benchmark代码,比较利用sync.Pool创建RequestCtx和直接创建:
import (
"sync"
"testing"
"github.com/valyala/fasthttp"
)
func BenchmarkPool(b *testing.B) {
ctxPool := sync.Pool{
New: func() interface{} {
return new(fasthttp.RequestCtx)
},
}
for i := 0; i < b.N; i++ {
ctx := ctxPool.Get()
ctxPool.Put(ctx)
}
}
func BenchmarkNoPool(b *testing.B) {
var ctx *fasthttp.RequestCtx
for i := 0; i < b.N; i++ {
ctx = new(fasthttp.RequestCtx)
_ = ctx
}
}
执行benchmark:
go test -bench=. -benchtime=10s -benchmem syncpool_test.go
结果如下:
BenchmarkPool-12 958146309 11.90 ns/op 0 B/op 0 allocs/op
BenchmarkNoPool-12 38607038 281.9 ns/op 1408 B/op 1 allocs/op
可以看出,使用sync.Pool性能提升了20多倍。
众所周知,http请求的header以及body其实就是一个kv字典,所以一般用map[string]string或者map[string][]string来表示。但是fasthttp使用了[]slice来存储kv,这样做的好处是:当参数使用完需要清理时不用释放内存,只需将长度变为0,其申请的内存还在,下次使用的时候直接覆盖就可以了,这样便于复用,避免重新申请内存。来看具体的代码:
type Args struct {
noCopy noCopy //nolint:unused,structcheck
args []argsKV
buf []byte
}
type argsKV struct {
key []byte
value []byte
noValue bool
}
// Reset clears query args. 清理时只需要将长度变为0
func (a *Args) Reset() {
a.args = a.args[:0]
}
func allocArg(h []argsKV) ([]argsKV, *argsKV) {
n := len(h)
if cap(h) > n { //分配时,如果容量够,那么不需要重新申请内存,直接增加长度即可
h = h[:n+1]
} else { //容量不够时才需重新分配
h = append(h, argsKV{
value: []byte{},
})
}
return h, &h[n]
}
这样做有一个弊端:当要查询的时候,时间复杂度是O(N),因为要遍历整个slice。但是一般一个请求kv项不会特别多,所以查询的性能损耗不明显:
func peekArgBytes(h []argsKV, k []byte) []byte {
for i, n := 0, len(h); i < n; i++ {
kv := &h[i]
if bytes.Equal(kv.key, k) {
return kv.value
}
}
return nil
}
在golang中,string是不可变的。也就是说要修改一个string,需要重新分配一块内存。而使用[]byte有两个好处:
var h []byte
func Reset(h []byte){
h = h[:0]
}
string的底层其实是StringHeader结构:
type StringHeader struct {
Data uintptr
Len int
}
而slice底层是SliceHeader结构:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
如果直接用类型强转,则需新申请一块内存来存放Data数据。
然而两者结构相似,只是slice多了一个Cap属性。fasthttp基于这一点优化了两者的相互转换。
[]byte转换为string时,只需要将指针强转即可,相当于丢弃了Cap属性:
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
string转换为[]byte时,则新建一个string结构体,但是将[]byte的Data和Len赋给新建的结构体,无需新申请一块Data内存。
func s2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
注:[]byte和string的优化依赖golang底层对两者的实现,如果后面的版本中底层实现有所变动,可能该优化不再适用。
在bufio库中,Peek和ReadBytes都是读取字节,不同的是Peek是直接返回Reader的buf,而ReadBytes新申请一块内存,并将要读的那些字节从reader.buf中拷贝到新申请的内存中。所以,适用Peek的好处是读取字节时,可避免新申请内存。
func (b *Reader) Peek(n int) ([]byte, error) {
// ...
// 0 <= n <= len(b.buf)
var err error
if avail := b.w - b.r; avail < n {
// not enough data in buffer
n = avail
err = b.readErr()
if err == nil {
err = ErrBufferFull
}
}
return b.buf[b.r : b.r+n], err
}
func (b *Reader) ReadBytes(delim byte) ([]byte, error) {
full, frag, n, err := b.collectFragments(delim)
// Allocate new buffer to hold the full pieces and the fragment.
buf := make([]byte, n)
n = 0
// Copy full pieces and fragment in.
for i := range full {
n += copy(buf[n:], full[i])
}
copy(buf[n:], frag)
return buf, err
}
在fasthttp的基础上,这里罗列一些关于golang开发提升性能的best pratice:
go tool pprof --alloc_objects your-program mem.pprof # 分析对象分配
go tool pprof your-program cpu.pprof #分析cpu
var (
// 均未初始化
dst []byte
src []byte
)
// 以下case,即使dst或者src是nil,都是没有问题的
dst = append(dst, src...)
copy(dst, src)
(string(src) == "")
(len(src) == 0)
src = src[:0]
// src是nil时也不会panic
for i, ch := range src {
doSomething(i, ch)
}
所以像下面的长度检查是不需要的:srcLen := 0
if src != nil {
srcLen = len(src)
}
直接这样用就可以:srcLen := len(src)
dst = append(dst, "foobar"...)
buf := make([]byte, 100)
a := buf[:10] // len(a) == 10, cap(a) == 100.
b := a[:100] // is valid, since cap(a) == 100.
fasthttp是一个优秀的http框架,虽然它的一些用法和标准库不太一样,但是其底层优化性能的技巧很值得借鉴,在优化服务性能的时候,这些经验都可以拿来学习。