Commit 07a22bbc authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: re-simplify HTTP/1.x status line writing

It used to be simple, and then it got complicated for speed (to reduce
allocations, mostly), but that involved a mutex and hurt multi-core
performance, contending on the mutex.

A change was sent to try to improve that mutex contention in
https://go-review.googlesource.com/c/42110/2/src/net/http/server.go
but that introduced its own allocations (the string->interface{}
boxing for the sync.Map key), which runs counter to the whole point of
that statusLine function: to remove allocations.

Instead, make the code simple again and not have a mutex. It's a bit
slower for the single-core case, but nobody with a single-user HTTP
server cares about 50 nanoseconds:

name                  old time/op    new time/op    delta
ResponseStatusLine      37.5ns ± 2%    87.1ns ± 2%  +132.42%          (p=0.029 n=4+4)
ResponseStatusLine-2    63.1ns ± 1%    43.1ns ±12%   -31.67%          (p=0.029 n=4+4)
ResponseStatusLine-4    53.8ns ± 8%    40.2ns ± 2%   -25.29%          (p=0.029 n=4+4)

name                  old alloc/op   new alloc/op   delta
ResponseStatusLine      0.00B ±NaN%    0.00B ±NaN%      ~     (all samples are equal)
ResponseStatusLine-2    0.00B ±NaN%    0.00B ±NaN%      ~     (all samples are equal)
ResponseStatusLine-4    0.00B ±NaN%    0.00B ±NaN%      ~     (all samples are equal)

name                  old allocs/op  new allocs/op  delta
ResponseStatusLine       0.00 ±NaN%     0.00 ±NaN%      ~     (all samples are equal)
ResponseStatusLine-2     0.00 ±NaN%     0.00 ±NaN%      ~     (all samples are equal)
ResponseStatusLine-4     0.00 ±NaN%     0.00 ±NaN%      ~     (all samples are equal)

(Note the code could be even simpler with fmt.Fprintf, but that is
 relatively slow and involves a bunch of allocations getting arguments
 into interface{} for the call)

Change-Id: I1fa119132dbbf97a8e7204ce3e0707d433060da2
Reviewed-on: https://go-review.googlesource.com/42133
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarBryan Mills <bcmills@google.com>
parent 16b6bb88
...@@ -17,17 +17,19 @@ import ( ...@@ -17,17 +17,19 @@ import (
) )
var ( var (
DefaultUserAgent = defaultUserAgent DefaultUserAgent = defaultUserAgent
NewLoggingConn = newLoggingConn NewLoggingConn = newLoggingConn
ExportAppendTime = appendTime ExportAppendTime = appendTime
ExportRefererForURL = refererForURL ExportRefererForURL = refererForURL
ExportServerNewConn = (*Server).newConn ExportServerNewConn = (*Server).newConn
ExportCloseWriteAndWait = (*conn).closeWriteAndWait ExportCloseWriteAndWait = (*conn).closeWriteAndWait
ExportErrRequestCanceled = errRequestCanceled ExportErrRequestCanceled = errRequestCanceled
ExportErrRequestCanceledConn = errRequestCanceledConn ExportErrRequestCanceledConn = errRequestCanceledConn
ExportServeFile = serveFile ExportServeFile = serveFile
ExportScanETag = scanETag ExportScanETag = scanETag
ExportHttp2ConfigureServer = http2ConfigureServer ExportHttp2ConfigureServer = http2ConfigureServer
Export_shouldCopyHeaderOnRedirect = shouldCopyHeaderOnRedirect
Export_writeStatusLine = writeStatusLine
) )
func init() { func init() {
...@@ -188,8 +190,6 @@ func ExportHttp2ConfigureTransport(t *Transport) error { ...@@ -188,8 +190,6 @@ func ExportHttp2ConfigureTransport(t *Transport) error {
return nil return nil
} }
var Export_shouldCopyHeaderOnRedirect = shouldCopyHeaderOnRedirect
func (s *Server) ExportAllConnsIdle() bool { func (s *Server) ExportAllConnsIdle() bool {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
......
...@@ -5539,3 +5539,14 @@ func TestServerValidatesMethod(t *testing.T) { ...@@ -5539,3 +5539,14 @@ func TestServerValidatesMethod(t *testing.T) {
} }
} }
} }
func BenchmarkResponseStatusLine(b *testing.B) {
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
bw := bufio.NewWriter(ioutil.Discard)
var buf3 [3]byte
for pb.Next() {
Export_writeStatusLine(bw, true, 200, buf3[:])
}
})
}
...@@ -439,9 +439,10 @@ type response struct { ...@@ -439,9 +439,10 @@ type response struct {
handlerDone atomicBool // set true when the handler exits handlerDone atomicBool // set true when the handler exits
// Buffers for Date and Content-Length // Buffers for Date, Content-Length, and status code
dateBuf [len(TimeFormat)]byte dateBuf [len(TimeFormat)]byte
clenBuf [10]byte clenBuf [10]byte
statusBuf [3]byte
// closeNotifyCh is the channel returned by CloseNotify. // closeNotifyCh is the channel returned by CloseNotify.
// TODO(bradfitz): this is currently (for Go 1.8) always // TODO(bradfitz): this is currently (for Go 1.8) always
...@@ -1379,7 +1380,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -1379,7 +1380,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
} }
} }
w.conn.bufw.WriteString(statusLine(w.req, code)) writeStatusLine(w.conn.bufw, w.req.ProtoAtLeast(1, 1), code, w.statusBuf[:])
cw.header.WriteSubset(w.conn.bufw, excludeHeader) cw.header.WriteSubset(w.conn.bufw, excludeHeader)
setHeader.Write(w.conn.bufw) setHeader.Write(w.conn.bufw)
w.conn.bufw.Write(crlf) w.conn.bufw.Write(crlf)
...@@ -1403,49 +1404,25 @@ func foreachHeaderElement(v string, fn func(string)) { ...@@ -1403,49 +1404,25 @@ func foreachHeaderElement(v string, fn func(string)) {
} }
} }
// statusLines is a cache of Status-Line strings, keyed by code (for // writeStatusLine writes an HTTP/1.x Status-Line (RFC 2616 Section 6.1)
// HTTP/1.1) or negative code (for HTTP/1.0). This is faster than a // to bw. is11 is whether the HTTP request is HTTP/1.1. false means HTTP/1.0.
// map keyed by struct of two fields. This map's max size is bounded // code is the response status code.
// by 2*len(statusText), two protocol types for each known official // scratch is an optional scratch buffer. If it has at least capacity 3, it's used.
// status code in the statusText map. func writeStatusLine(bw *bufio.Writer, is11 bool, code int, scratch []byte) {
var ( if is11 {
statusMu sync.RWMutex bw.WriteString("HTTP/1.1 ")
statusLines = make(map[int]string) } else {
) bw.WriteString("HTTP/1.0 ")
// statusLine returns a response Status-Line (RFC 2616 Section 6.1)
// for the given request and response status code.
func statusLine(req *Request, code int) string {
// Fast path:
key := code
proto11 := req.ProtoAtLeast(1, 1)
if !proto11 {
key = -key
}
statusMu.RLock()
line, ok := statusLines[key]
statusMu.RUnlock()
if ok {
return line
}
// Slow path:
proto := "HTTP/1.0"
if proto11 {
proto = "HTTP/1.1"
}
codestring := fmt.Sprintf("%03d", code)
text, ok := statusText[code]
if !ok {
text = "status code " + codestring
} }
line = proto + " " + codestring + " " + text + "\r\n" if text, ok := statusText[code]; ok {
if ok { bw.Write(strconv.AppendInt(scratch[:0], int64(code), 10))
statusMu.Lock() bw.WriteByte(' ')
defer statusMu.Unlock() bw.WriteString(text)
statusLines[key] = line bw.WriteString("\r\n")
} else {
// don't worry about performance
fmt.Fprintf(bw, "%03d status code %d\r\n", code, code)
} }
return line
} }
// bodyAllowed reports whether a Write is allowed for this response type. // bodyAllowed reports whether a Write is allowed for this response type.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment