Skip to content

Commit 07a22bb

Browse files
committed
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan Mills <[email protected]>
1 parent 16b6bb8 commit 07a22bb

File tree

3 files changed

+46
-58
lines changed

3 files changed

+46
-58
lines changed

src/net/http/export_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ import (
1717
)
1818

1919
var (
20-
DefaultUserAgent = defaultUserAgent
21-
NewLoggingConn = newLoggingConn
22-
ExportAppendTime = appendTime
23-
ExportRefererForURL = refererForURL
24-
ExportServerNewConn = (*Server).newConn
25-
ExportCloseWriteAndWait = (*conn).closeWriteAndWait
26-
ExportErrRequestCanceled = errRequestCanceled
27-
ExportErrRequestCanceledConn = errRequestCanceledConn
28-
ExportServeFile = serveFile
29-
ExportScanETag = scanETag
30-
ExportHttp2ConfigureServer = http2ConfigureServer
20+
DefaultUserAgent = defaultUserAgent
21+
NewLoggingConn = newLoggingConn
22+
ExportAppendTime = appendTime
23+
ExportRefererForURL = refererForURL
24+
ExportServerNewConn = (*Server).newConn
25+
ExportCloseWriteAndWait = (*conn).closeWriteAndWait
26+
ExportErrRequestCanceled = errRequestCanceled
27+
ExportErrRequestCanceledConn = errRequestCanceledConn
28+
ExportServeFile = serveFile
29+
ExportScanETag = scanETag
30+
ExportHttp2ConfigureServer = http2ConfigureServer
31+
Export_shouldCopyHeaderOnRedirect = shouldCopyHeaderOnRedirect
32+
Export_writeStatusLine = writeStatusLine
3133
)
3234

3335
func init() {
@@ -188,8 +190,6 @@ func ExportHttp2ConfigureTransport(t *Transport) error {
188190
return nil
189191
}
190192

191-
var Export_shouldCopyHeaderOnRedirect = shouldCopyHeaderOnRedirect
192-
193193
func (s *Server) ExportAllConnsIdle() bool {
194194
s.mu.Lock()
195195
defer s.mu.Unlock()

src/net/http/serve_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5539,3 +5539,14 @@ func TestServerValidatesMethod(t *testing.T) {
55395539
}
55405540
}
55415541
}
5542+
5543+
func BenchmarkResponseStatusLine(b *testing.B) {
5544+
b.ReportAllocs()
5545+
b.RunParallel(func(pb *testing.PB) {
5546+
bw := bufio.NewWriter(ioutil.Discard)
5547+
var buf3 [3]byte
5548+
for pb.Next() {
5549+
Export_writeStatusLine(bw, true, 200, buf3[:])
5550+
}
5551+
})
5552+
}

src/net/http/server.go

Lines changed: 22 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -439,9 +439,10 @@ type response struct {
439439

440440
handlerDone atomicBool // set true when the handler exits
441441

442-
// Buffers for Date and Content-Length
443-
dateBuf [len(TimeFormat)]byte
444-
clenBuf [10]byte
442+
// Buffers for Date, Content-Length, and status code
443+
dateBuf [len(TimeFormat)]byte
444+
clenBuf [10]byte
445+
statusBuf [3]byte
445446

446447
// closeNotifyCh is the channel returned by CloseNotify.
447448
// TODO(bradfitz): this is currently (for Go 1.8) always
@@ -1379,7 +1380,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
13791380
}
13801381
}
13811382

1382-
w.conn.bufw.WriteString(statusLine(w.req, code))
1383+
writeStatusLine(w.conn.bufw, w.req.ProtoAtLeast(1, 1), code, w.statusBuf[:])
13831384
cw.header.WriteSubset(w.conn.bufw, excludeHeader)
13841385
setHeader.Write(w.conn.bufw)
13851386
w.conn.bufw.Write(crlf)
@@ -1403,49 +1404,25 @@ func foreachHeaderElement(v string, fn func(string)) {
14031404
}
14041405
}
14051406

1406-
// statusLines is a cache of Status-Line strings, keyed by code (for
1407-
// HTTP/1.1) or negative code (for HTTP/1.0). This is faster than a
1408-
// map keyed by struct of two fields. This map's max size is bounded
1409-
// by 2*len(statusText), two protocol types for each known official
1410-
// status code in the statusText map.
1411-
var (
1412-
statusMu sync.RWMutex
1413-
statusLines = make(map[int]string)
1414-
)
1415-
1416-
// statusLine returns a response Status-Line (RFC 2616 Section 6.1)
1417-
// for the given request and response status code.
1418-
func statusLine(req *Request, code int) string {
1419-
// Fast path:
1420-
key := code
1421-
proto11 := req.ProtoAtLeast(1, 1)
1422-
if !proto11 {
1423-
key = -key
1424-
}
1425-
statusMu.RLock()
1426-
line, ok := statusLines[key]
1427-
statusMu.RUnlock()
1428-
if ok {
1429-
return line
1430-
}
1431-
1432-
// Slow path:
1433-
proto := "HTTP/1.0"
1434-
if proto11 {
1435-
proto = "HTTP/1.1"
1436-
}
1437-
codestring := fmt.Sprintf("%03d", code)
1438-
text, ok := statusText[code]
1439-
if !ok {
1440-
text = "status code " + codestring
1407+
// writeStatusLine writes an HTTP/1.x Status-Line (RFC 2616 Section 6.1)
1408+
// to bw. is11 is whether the HTTP request is HTTP/1.1. false means HTTP/1.0.
1409+
// code is the response status code.
1410+
// scratch is an optional scratch buffer. If it has at least capacity 3, it's used.
1411+
func writeStatusLine(bw *bufio.Writer, is11 bool, code int, scratch []byte) {
1412+
if is11 {
1413+
bw.WriteString("HTTP/1.1 ")
1414+
} else {
1415+
bw.WriteString("HTTP/1.0 ")
14411416
}
1442-
line = proto + " " + codestring + " " + text + "\r\n"
1443-
if ok {
1444-
statusMu.Lock()
1445-
defer statusMu.Unlock()
1446-
statusLines[key] = line
1417+
if text, ok := statusText[code]; ok {
1418+
bw.Write(strconv.AppendInt(scratch[:0], int64(code), 10))
1419+
bw.WriteByte(' ')
1420+
bw.WriteString(text)
1421+
bw.WriteString("\r\n")
1422+
} else {
1423+
// don't worry about performance
1424+
fmt.Fprintf(bw, "%03d status code %d\r\n", code, code)
14471425
}
1448-
return line
14491426
}
14501427

14511428
// bodyAllowed reports whether a Write is allowed for this response type.

0 commit comments

Comments
 (0)