Commit babbd55e authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: fewer allocations in chunkWriter.WriteHeader

It was unnecessarily cloning and then mutating a map that had
a very short lifetime (just that function).

No new tests, because they were added in revision 833bf2ef1527
(TestHeaderToWire). The benchmarks below are from the earlier
commit, revision 52e3407d.

I noticed this inefficiency when reviewing a change Peter Buhr
is looking into, which will also use these benchmarks.

benchmark                         old ns/op    new ns/op    delta
BenchmarkServerHandlerTypeLen         12547        12325   -1.77%
BenchmarkServerHandlerNoLen           12466        11167  -10.42%
BenchmarkServerHandlerNoType          12699        11800   -7.08%
BenchmarkServerHandlerNoHeader        11901         9210  -22.61%

benchmark                        old allocs   new allocs    delta
BenchmarkServerHandlerTypeLen            21           20   -4.76%
BenchmarkServerHandlerNoLen              20           18  -10.00%
BenchmarkServerHandlerNoType             20           18  -10.00%
BenchmarkServerHandlerNoHeader           17           13  -23.53%

benchmark                         old bytes    new bytes    delta
BenchmarkServerHandlerTypeLen          1930         1913   -0.88%
BenchmarkServerHandlerNoLen            1912         1879   -1.73%
BenchmarkServerHandlerNoType           1912         1878   -1.78%
BenchmarkServerHandlerNoHeader         1491         1086  -27.16%

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/8268046
parent 3d5daa23
...@@ -224,8 +224,10 @@ const bufferBeforeChunkingSize = 2048 ...@@ -224,8 +224,10 @@ const bufferBeforeChunkingSize = 2048
type chunkWriter struct { type chunkWriter struct {
res *response res *response
// header is either the same as res.handlerHeader, // header is either nil or a deep clone of res.handlerHeader
// or a deep clone if the handler called Header. // at the time of res.WriteHeader, if res.WriteHeader is
// called and extra buffering is being done to calculate
// Content-Type and/or Content-Length.
header Header header Header
// wroteHeader tells whether the header's been written to "the // wroteHeader tells whether the header's been written to "the
...@@ -238,7 +240,10 @@ type chunkWriter struct { ...@@ -238,7 +240,10 @@ type chunkWriter struct {
chunking bool // using chunked transfer encoding for reply body chunking bool // using chunked transfer encoding for reply body
} }
var crlf = []byte("\r\n") var (
crlf = []byte("\r\n")
colonSpace = []byte(": ")
)
func (cw *chunkWriter) Write(p []byte) (n int, err error) { func (cw *chunkWriter) Write(p []byte) (n int, err error) {
if !cw.wroteHeader { if !cw.wroteHeader {
...@@ -613,6 +618,37 @@ func (w *response) WriteHeader(code int) { ...@@ -613,6 +618,37 @@ func (w *response) WriteHeader(code int) {
} }
} }
// extraHeader is the set of headers sometimes added by chunkWriter.writeHeader.
// This type is used to avoid extra allocations from cloning and/or populating
// the response Header map and all its 1-element slices.
type extraHeader struct {
contentType string
contentLength string
connection string
date string
transferEncoding string
}
// Sorted the same as extraHeader.Write's loop.
var extraHeaderKeys = [][]byte{
[]byte("Content-Type"), []byte("Content-Length"),
[]byte("Connection"), []byte("Date"), []byte("Transfer-Encoding"),
}
// The value receiver, despite copying 5 strings to the stack,
// prevents an extra allocation. The escape analysis isn't smart
// enough to realize this doesn't mutate h.
func (h extraHeader) Write(w io.Writer) {
for i, v := range []string{h.contentType, h.contentLength, h.connection, h.date, h.transferEncoding} {
if v != "" {
w.Write(extraHeaderKeys[i])
w.Write(colonSpace)
io.WriteString(w, v)
w.Write(crlf)
}
}
}
// writeHeader finalizes the header sent to the client and writes it // writeHeader finalizes the header sent to the client and writes it
// to cw.res.conn.buf. // to cw.res.conn.buf.
// //
...@@ -629,37 +665,46 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -629,37 +665,46 @@ func (cw *chunkWriter) writeHeader(p []byte) {
w := cw.res w := cw.res
if cw.header == nil { // header is written out to w.conn.buf below. Depending on the
if w.handlerDone { // state of the handler, we either own the map or not. If we
// The handler won't be making further changes to the // don't own it, the exclude map is created lazily for
// response header map, so we use it directly. // WriteSubset to remove headers. The setHeader struct holds
cw.header = w.handlerHeader // headers we need to add.
} else { header := cw.header
// Snapshot the header map, since it might be some owned := header != nil
// time before we actually write w.cw to the wire and if !owned {
// we don't want the handler's potential future header = w.handlerHeader
// (arguably buggy) modifications to the map to make }
// it into the written headers. This preserves var excludeHeader map[string]bool
// compatibility with Go 1.0, which always flushed the delHeader := func(key string) {
// headers on a call to rw.WriteHeader. if owned {
cw.header = w.handlerHeader.clone() header.Del(key)
return
} }
if _, ok := header[key]; !ok {
return
}
if excludeHeader == nil {
excludeHeader = make(map[string]bool)
}
excludeHeader[key] = true
} }
var setHeader extraHeader
// If the handler is done but never sent a Content-Length // If the handler is done but never sent a Content-Length
// response header and this is our first (and last) write, set // response header and this is our first (and last) write, set
// it, even to zero. This helps HTTP/1.0 clients keep their // it, even to zero. This helps HTTP/1.0 clients keep their
// "keep-alive" connections alive. // "keep-alive" connections alive.
if w.handlerDone && cw.header.get("Content-Length") == "" && w.req.Method != "HEAD" { if w.handlerDone && header.get("Content-Length") == "" && w.req.Method != "HEAD" {
w.contentLength = int64(len(p)) w.contentLength = int64(len(p))
cw.header.Set("Content-Length", strconv.Itoa(len(p))) setHeader.contentLength = strconv.Itoa(len(p))
} }
// If this was an HTTP/1.0 request with keep-alive and we sent a // If this was an HTTP/1.0 request with keep-alive and we sent a
// Content-Length back, we can make this a keep-alive response ... // Content-Length back, we can make this a keep-alive response ...
if w.req.wantsHttp10KeepAlive() { if w.req.wantsHttp10KeepAlive() {
sentLength := cw.header.get("Content-Length") != "" sentLength := header.get("Content-Length") != ""
if sentLength && cw.header.get("Connection") == "keep-alive" { if sentLength && header.get("Connection") == "keep-alive" {
w.closeAfterReply = false w.closeAfterReply = false
} }
} }
...@@ -668,15 +713,15 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -668,15 +713,15 @@ func (cw *chunkWriter) writeHeader(p []byte) {
hasCL := w.contentLength != -1 hasCL := w.contentLength != -1
if w.req.wantsHttp10KeepAlive() && (w.req.Method == "HEAD" || hasCL) { if w.req.wantsHttp10KeepAlive() && (w.req.Method == "HEAD" || hasCL) {
_, connectionHeaderSet := cw.header["Connection"] _, connectionHeaderSet := header["Connection"]
if !connectionHeaderSet { if !connectionHeaderSet {
cw.header.Set("Connection", "keep-alive") setHeader.connection = "keep-alive"
} }
} else if !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() { } else if !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() {
w.closeAfterReply = true w.closeAfterReply = true
} }
if cw.header.get("Connection") == "close" { if header.get("Connection") == "close" {
w.closeAfterReply = true w.closeAfterReply = true
} }
...@@ -690,7 +735,8 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -690,7 +735,8 @@ func (cw *chunkWriter) writeHeader(p []byte) {
n, _ := io.CopyN(ioutil.Discard, w.req.Body, maxPostHandlerReadBytes+1) n, _ := io.CopyN(ioutil.Discard, w.req.Body, maxPostHandlerReadBytes+1)
if n >= maxPostHandlerReadBytes { if n >= maxPostHandlerReadBytes {
w.requestTooLarge() w.requestTooLarge()
cw.header.Set("Connection", "close") delHeader("Connection")
setHeader.connection = "close"
} else { } else {
w.req.Body.Close() w.req.Body.Close()
} }
...@@ -700,40 +746,38 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -700,40 +746,38 @@ func (cw *chunkWriter) writeHeader(p []byte) {
code := w.status code := w.status
if code == StatusNotModified { if code == StatusNotModified {
// Must not have body. // Must not have body.
for _, header := range []string{"Content-Type", "Content-Length", "Transfer-Encoding"} { // RFC 2616 section 10.3.5: "the response MUST NOT include other entity-headers"
// RFC 2616 section 10.3.5: "the response MUST NOT include other entity-headers" for _, k := range []string{"Content-Type", "Content-Length", "Transfer-Encoding"} {
if cw.header.get(header) != "" { delHeader(k)
cw.header.Del(header)
}
} }
} else { } else {
// If no content type, apply sniffing algorithm to body. // If no content type, apply sniffing algorithm to body.
if cw.header.get("Content-Type") == "" && w.req.Method != "HEAD" { if header.get("Content-Type") == "" && w.req.Method != "HEAD" {
cw.header.Set("Content-Type", DetectContentType(p)) setHeader.contentType = DetectContentType(p)
} }
} }
if _, ok := cw.header["Date"]; !ok { if _, ok := header["Date"]; !ok {
cw.header.Set("Date", time.Now().UTC().Format(TimeFormat)) setHeader.date = time.Now().UTC().Format(TimeFormat)
} }
te := cw.header.get("Transfer-Encoding") te := header.get("Transfer-Encoding")
hasTE := te != "" hasTE := te != ""
if hasCL && hasTE && te != "identity" { if hasCL && hasTE && te != "identity" {
// TODO: return an error if WriteHeader gets a return parameter // TODO: return an error if WriteHeader gets a return parameter
// For now just ignore the Content-Length. // For now just ignore the Content-Length.
log.Printf("http: WriteHeader called with both Transfer-Encoding of %q and a Content-Length of %d", log.Printf("http: WriteHeader called with both Transfer-Encoding of %q and a Content-Length of %d",
te, w.contentLength) te, w.contentLength)
cw.header.Del("Content-Length") delHeader("Content-Length")
hasCL = false hasCL = false
} }
if w.req.Method == "HEAD" || code == StatusNotModified { if w.req.Method == "HEAD" || code == StatusNotModified {
// do nothing // do nothing
} else if code == StatusNoContent { } else if code == StatusNoContent {
cw.header.Del("Transfer-Encoding") delHeader("Transfer-Encoding")
} else if hasCL { } else if hasCL {
cw.header.Del("Transfer-Encoding") delHeader("Transfer-Encoding")
} else if w.req.ProtoAtLeast(1, 1) { } else if w.req.ProtoAtLeast(1, 1) {
// HTTP/1.1 or greater: use chunked transfer encoding // HTTP/1.1 or greater: use chunked transfer encoding
// to avoid closing the connection at EOF. // to avoid closing the connection at EOF.
...@@ -741,29 +785,31 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -741,29 +785,31 @@ func (cw *chunkWriter) writeHeader(p []byte) {
// might have set. Deal with that as need arises once we have a valid // might have set. Deal with that as need arises once we have a valid
// use case. // use case.
cw.chunking = true cw.chunking = true
cw.header.Set("Transfer-Encoding", "chunked") setHeader.transferEncoding = "chunked"
} else { } else {
// HTTP version < 1.1: cannot do chunked transfer // HTTP version < 1.1: cannot do chunked transfer
// encoding and we don't know the Content-Length so // encoding and we don't know the Content-Length so
// signal EOF by closing connection. // signal EOF by closing connection.
w.closeAfterReply = true w.closeAfterReply = true
cw.header.Del("Transfer-Encoding") // in case already set delHeader("Transfer-Encoding") // in case already set
} }
// Cannot use Content-Length with non-identity Transfer-Encoding. // Cannot use Content-Length with non-identity Transfer-Encoding.
if cw.chunking { if cw.chunking {
cw.header.Del("Content-Length") delHeader("Content-Length")
} }
if !w.req.ProtoAtLeast(1, 0) { if !w.req.ProtoAtLeast(1, 0) {
return return
} }
if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") { if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") {
cw.header.Set("Connection", "close") delHeader("Connection")
setHeader.connection = "close"
} }
io.WriteString(w.conn.buf, statusLine(w.req, code)) io.WriteString(w.conn.buf, statusLine(w.req, code))
cw.header.Write(w.conn.buf) cw.header.WriteSubset(w.conn.buf, excludeHeader)
setHeader.Write(w.conn.buf)
w.conn.buf.Write(crlf) w.conn.buf.Write(crlf)
} }
......
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