When FASthTTP started out, it claimed to be ten times faster than NET/HTTP, with less memory allocation. In the meantime, give some tips on go development on Github.
This article takes a peek at how these techniques are used in FasthttP through source code.
Reduce the allocation of [] bytes and reuse them as much as possible
There are two ways to reuse:
- sync.Pool
- Slice slice of = [: 0]. All types of Reset methods use this method. For example, type URIs, Args, byteBuffers, cookies, RequestHeader, ResponseHeader, etc.
Sync.pool is used in 35 places in Fasthttp. In addition to reducing GC stress, sync.pool can reuse objects and reduce memory allocation.
Server type Server struct {//... CtxPool sync.Pool readerPool sync.Pool readerPool bufio object for reading HTTP Request writerPool sync.Pool bufio object for reading HTTP Request writerPool sync.Pool HTTP Request hijackConnPool sync.Pool bytePool sync.Pool} // For example, cookies var cookiePool = &sync.Pool{New: func() interface{} { return &Cookie{} }, } func AcquireCookie() *Cookie { return cookiePool.Get().(*Cookie) } func ReleaseCookie(c *Cookie) { c.Reset() Cookipool.put (c)} // For example, workPool. Each request runs with a new Goroutine. Type workerPool struct {//... workerChanPool sync.Pool } func (wp *workerPool) getCh() *workerChan { var ch *workerChan // ... if ch == nil { if ! CreateWorker {// The number of workers has reached the upper limit, Wp.workerchanpool.get () if VCH == nil {VCH = &workerchan {ch: make(chan net.Conn, workerChanCap), Wp.workerfunc (ch) {wp.workerChanpool.put (VCH)}() {workerchanpool.put (VCH)}()} return ch }Copy the code
And reuse already-allocated [] bytes.
Append (s[:0], b…) These two ways of reuse, 191 times in total.
// Clear URI func (u *URI) Reset() {u.pathOriginal = U.pathOriginal [:0] U. path = U. path[:0] U. path[:0] // . ResponseHeader func (h *ResponseHeader) resetSkipNormalize() {h.nohttp11 = false h.connectionClose = false h.statusCode = 0 h.contentLength = 0 h.contentLengthBytes = h.contentLengthBytes[:0] h.contentType = h.contentType[:0] H.server = H.server [:0] h.h = H.h [:0] h.cookies = H.cookies [:0]} // Clear Cookies func (c *Cookie) Reset() {c.key = c.key[:0] c.value = c.value[:0] c.expire = zeroTime c.maxAge = 0 c.domain = c.domain[:0] c.path = c.path[:0] c.httpOnly = false c.secure = false c.sameSite = CookieSameSiteDisabled } func (c *Cookie) SetKey(key string) { c.key = append(c.key[:0], key...) }Copy the code
Use []byte as the method parameter. Bytes.buffer can be avoided in pure write scenarios
Method arguments use []byte, which avoids the memory allocation and copying associated with []byte to string conversions. After all, the data originally read from Net.conn is also of []byte type.
Some places do want to pass string arguments, and Fasthttp also provides XXString() methods.
A = append(a, String…) . Doing so will not result in a string to a byte [] transformation (this conclusion by looking at the assembly, assembly and useless to the runtime. Stringtoslicebyte method)
Func (resp *Response) SetBodyString(body String) {//... bodyBuf.WriteString(body) }Copy the code
The bodyBuf variable above is of type ByteBuffer, derived from another library, ByteBufferPool, written by the author.
As introduced, the main goal of the library is to combat redundant memory allocation behavior. Compared to the standard library bytes.buffer type, performance is 30% higher.
But ByteBuffer only provides write class operations. Suitable for high-frequency write scenarios.
Take a look at how the bytes.buffer library grows the underlying slice. The important thing is that bytes.buffer has no memory overcommitment.
Func (b *Buffer) grow(n int) int {//... if m+n <= cap(b.buf)/2 { copy(b.buf[:], B.buf [b.off:])} else {makeSlice := makeSlice(2*cap(b.buf :]) + n); b.buf[b.off:]) b.buf = buf } // ... } func makeSlice(n int) [] maekSlice {return make([]byte, n)}Copy the code
Now look at what ByteBuffer does. The emphasis is on memory reuse.
Func (b *ByteBuffer) Reset() {b.B = b.B[:0]} Func (b *ByteBuffer) WriteString(s String) (int, error) {b.B = append(b.B, s... Return len(s), nil} // What if the buffer is large? // But because Reset() is used again, cap is sufficient. Func (b *ByteBuffer) Write(p []byte) (int, error) {b.B = Append (b.B, p...) return len(p), nil }Copy the code
Both Request and Response are stored in ByteBuffer. Clearing the body returns the ByteBuffer to the pool for reuse.
var ( responseBodyPool bytebufferpool.Pool requestBodyPool bytebufferpool.Pool ) func (req *Request) ResetBody() { req.RemoveMultipartFormFiles() req.closeBodyStream() if req.body ! = nil { if req.keepBodyBuffer { req.body.Reset() } else { requestBodyPool.Put(req.body) req.body = nil } } } func (resp *Response) ResetBody() { resp.bodyRaw = nil resp.closeBodyStream() if resp.body ! = nil { if resp.keepBodyBuffer { resp.body.Reset() } else { responseBodyPool.Put(resp.body) resp.body = nil } } }Copy the code
Wherever memory can be reused
Some places need KV data, generally use map[string]string. But map is not good for reuse. So FasthTTP implements a map using slice
The disadvantage is the query time complexity O(n).
Slice can be a good way to reduce memory allocation, especially in large concurrent scenarios.
Type argsKV struct {key []byte value []byte noValue bool} appendArg(args []argsKV, key, value string, NoValue bool) []argsKV {var kv *argsKV args, kv = allocArg(args) var. Key = append(kv.key[:0], key... Kv.value = append(kv.value[:0], value...) if noValue {kv.value = kv.value[:0]} else {// If noValue {kv.value = kv.value[:0], value... } kv.noValue = noValue return args } func allocArg(h []argsKV) ([]argsKV, *argsKV) {n := len(h) if cap(h) > n { H = h[:n+1]} else {argsKV{} return h, &h[n]}Copy the code
Avoid string / []byte conversion overhead
These two conversions come with memory allocation and copy overhead, but there is a trick to avoid the overhead. Using a string and slice runtime structure only one Cap field apart.
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } // []byte -> string func b2s(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } // string -> []byte func s2b(s string) []byte { sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: sh.Data, Len: sh.Len, Cap: sh.Len, } return *(*[]byte)(unsafe.Pointer(&bh)) }Copy the code
Note the problems with this approach:
- The converted []byte cannot be modified
- Depending on the XXHeader structure, the Runtime change structure is affected
- It is also affected if the unsafe.Pointer function is changed
And then I’ll sum it up
- Fasthttp avoids most of the redundant memory allocation behavior and can reuse no allocation.
- Use the sync. The Pool.
- Try to avoid the overhead of converting []byte to string.
- Use []byte – related features.