From d4cbc80d106a3f3b53631aa60b400c790b14bb52 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 19 May 2013 20:15:40 -0700 Subject: [PATCH] net/http: fewer allocations in the server path Don't allocate for the Date or Content-Length headers. A custom Date header formatter replaces use of time.Format. benchmark old ns/op new ns/op delta BenchmarkClientServer 67791 64424 -4.97% BenchmarkClientServerParallel4 62956 58533 -7.03% BenchmarkClientServerParallel64 62043 54789 -11.69% BenchmarkServer 254609 229060 -10.03% BenchmarkServerFakeConnNoKeepAlive 17038 16316 -4.24% BenchmarkServerFakeConnWithKeepAlive 14184 13226 -6.75% BenchmarkServerFakeConnWithKeepAliveLite 8591 7532 -12.33% BenchmarkServerHandlerTypeLen 10750 9961 -7.34% BenchmarkServerHandlerNoLen 9535 8935 -6.29% BenchmarkServerHandlerNoType 9858 9362 -5.03% BenchmarkServerHandlerNoHeader 7754 6856 -11.58% benchmark old allocs new allocs delta BenchmarkClientServer 68 66 -2.94% BenchmarkClientServerParallel4 68 66 -2.94% BenchmarkClientServerParallel64 68 66 -2.94% BenchmarkServer 21 19 -9.52% BenchmarkServerFakeConnNoKeepAlive 32 30 -6.25% BenchmarkServerFakeConnWithKeepAlive 27 25 -7.41% BenchmarkServerFakeConnWithKeepAliveLite 12 10 -16.67% BenchmarkServerHandlerTypeLen 19 18 -5.26% BenchmarkServerHandlerNoLen 17 15 -11.76% BenchmarkServerHandlerNoType 17 16 -5.88% BenchmarkServerHandlerNoHeader 12 10 -16.67% Update #5195 R=nigeltao CC=golang-dev https://golang.org/cl/9432046 --- src/pkg/net/http/serve_test.go | 3 ++ src/pkg/net/http/server.go | 69 +++++++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/pkg/net/http/serve_test.go b/src/pkg/net/http/serve_test.go index d7b321597c4..64d9321f139 100644 --- a/src/pkg/net/http/serve_test.go +++ b/src/pkg/net/http/serve_test.go @@ -1727,6 +1727,7 @@ func TestAcceptMaxFds(t *testing.T) { } func BenchmarkClientServer(b *testing.B) { + b.ReportAllocs() b.StopTimer() ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { fmt.Fprintf(rw, "Hello world.\n") @@ -1761,6 +1762,7 @@ func BenchmarkClientServerParallel64(b *testing.B) { } func benchmarkClientServerParallel(b *testing.B, conc int) { + b.ReportAllocs() b.StopTimer() ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { fmt.Fprintf(rw, "Hello world.\n") @@ -1805,6 +1807,7 @@ func benchmarkClientServerParallel(b *testing.B, conc int) { // $ go tool pprof http.test http.prof // (pprof) web func BenchmarkServer(b *testing.B) { + b.ReportAllocs() // Child process mode; if url := os.Getenv("TEST_BENCH_SERVER_URL"); url != "" { n, err := strconv.Atoi(os.Getenv("TEST_BENCH_CLIENT_N")) diff --git a/src/pkg/net/http/server.go b/src/pkg/net/http/server.go index b2596070500..fe35562447f 100644 --- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -320,6 +320,10 @@ type response struct { requestBodyLimitHit bool handlerDone bool // set true when the handler exits + + // Buffers for Date and Content-Length + dateBuf [len(TimeFormat)]byte + clenBuf [10]byte } // requestTooLarge is called by maxBytesReader when too much input has @@ -525,6 +529,27 @@ func (ecr *expectContinueReader) Close() error { // It is like time.RFC1123 but hard codes GMT as the time zone. const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" +// appendTime is a non-allocating version of []byte(time.Now().UTC().Format(TimeFormat)) +func appendTime(b []byte, t time.Time) []byte { + const days = "SunMonTueWedThuFriSat" + const months = "JanFebMarAprMayJunJulAugSepOctNovDec" + + yy, mm, dd := t.Date() + hh, mn, ss := t.Clock() + day := days[3*t.Weekday():] + mon := months[3*(mm-1):] + + return append(b, + day[0], day[1], day[2], ',', ' ', + byte('0'+dd/10), byte('0'+dd%10), ' ', + mon[0], mon[1], mon[2], ' ', + byte('0'+yy/1000), byte('0'+(yy/100)%10), byte('0'+(yy/10)%10), byte('0'+yy%10), ' ', + byte('0'+hh/10), byte('0'+hh%10), ':', + byte('0'+mn/10), byte('0'+mn%10), ':', + byte('0'+ss/10), byte('0'+ss%10), ' ', + 'G', 'M', 'T') +} + var errTooLarge = errors.New("http: request too large") // Read next request from connection. @@ -620,27 +645,45 @@ func (w *response) WriteHeader(code int) { // the response Header map and all its 1-element slices. type extraHeader struct { contentType string - contentLength string connection string - date string transferEncoding string + date []byte // written if not nil + contentLength []byte // written if not nil } // Sorted the same as extraHeader.Write's loop. var extraHeaderKeys = [][]byte{ - []byte("Content-Type"), []byte("Content-Length"), - []byte("Connection"), []byte("Date"), []byte("Transfer-Encoding"), + []byte("Content-Type"), + []byte("Connection"), + []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} { +var ( + headerContentLength = []byte("Content-Length: ") + headerDate = []byte("Date: ") +) + +// Write writes the headers described in h to w. +// +// This method has a value receiver, despite the somewhat large size +// of h, because it prevents an allocation. The escape analysis isn't +// smart enough to realize this function doesn't mutate h. +func (h extraHeader) Write(w *bufio.Writer) { + if h.date != nil { + w.Write(headerDate) + w.Write(h.date) + w.Write(crlf) + } + if h.contentLength != nil { + w.Write(headerContentLength) + w.Write(h.contentLength) + w.Write(crlf) + } + for i, v := range []string{h.contentType, h.connection, h.transferEncoding} { if v != "" { w.Write(extraHeaderKeys[i]) w.Write(colonSpace) - io.WriteString(w, v) + w.WriteString(v) w.Write(crlf) } } @@ -694,7 +737,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { // "keep-alive" connections alive. if w.handlerDone && header.get("Content-Length") == "" && w.req.Method != "HEAD" { w.contentLength = int64(len(p)) - setHeader.contentLength = strconv.Itoa(len(p)) + setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10) } // If this was an HTTP/1.0 request with keep-alive and we sent a @@ -755,7 +798,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { } if _, ok := header["Date"]; !ok { - setHeader.date = time.Now().UTC().Format(TimeFormat) + setHeader.date = appendTime(cw.res.dateBuf[:0], time.Now()) } te := header.get("Transfer-Encoding") @@ -806,7 +849,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { io.WriteString(w.conn.buf, statusLine(w.req, code)) cw.header.WriteSubset(w.conn.buf, excludeHeader) - setHeader.Write(w.conn.buf) + setHeader.Write(w.conn.buf.Writer) w.conn.buf.Write(crlf) }