Commit d4cbc80d authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

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
parent 38abb09a
...@@ -1727,6 +1727,7 @@ func TestAcceptMaxFds(t *testing.T) { ...@@ -1727,6 +1727,7 @@ func TestAcceptMaxFds(t *testing.T) {
} }
func BenchmarkClientServer(b *testing.B) { func BenchmarkClientServer(b *testing.B) {
b.ReportAllocs()
b.StopTimer() b.StopTimer()
ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) {
fmt.Fprintf(rw, "Hello world.\n") fmt.Fprintf(rw, "Hello world.\n")
...@@ -1761,6 +1762,7 @@ func BenchmarkClientServerParallel64(b *testing.B) { ...@@ -1761,6 +1762,7 @@ func BenchmarkClientServerParallel64(b *testing.B) {
} }
func benchmarkClientServerParallel(b *testing.B, conc int) { func benchmarkClientServerParallel(b *testing.B, conc int) {
b.ReportAllocs()
b.StopTimer() b.StopTimer()
ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) {
fmt.Fprintf(rw, "Hello world.\n") fmt.Fprintf(rw, "Hello world.\n")
...@@ -1805,6 +1807,7 @@ func benchmarkClientServerParallel(b *testing.B, conc int) { ...@@ -1805,6 +1807,7 @@ func benchmarkClientServerParallel(b *testing.B, conc int) {
// $ go tool pprof http.test http.prof // $ go tool pprof http.test http.prof
// (pprof) web // (pprof) web
func BenchmarkServer(b *testing.B) { func BenchmarkServer(b *testing.B) {
b.ReportAllocs()
// Child process mode; // Child process mode;
if url := os.Getenv("TEST_BENCH_SERVER_URL"); url != "" { if url := os.Getenv("TEST_BENCH_SERVER_URL"); url != "" {
n, err := strconv.Atoi(os.Getenv("TEST_BENCH_CLIENT_N")) n, err := strconv.Atoi(os.Getenv("TEST_BENCH_CLIENT_N"))
......
...@@ -320,6 +320,10 @@ type response struct { ...@@ -320,6 +320,10 @@ type response struct {
requestBodyLimitHit bool requestBodyLimitHit bool
handlerDone bool // set true when the handler exits 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 // requestTooLarge is called by maxBytesReader when too much input has
...@@ -525,6 +529,27 @@ func (ecr *expectContinueReader) Close() error { ...@@ -525,6 +529,27 @@ func (ecr *expectContinueReader) Close() error {
// It is like time.RFC1123 but hard codes GMT as the time zone. // It is like time.RFC1123 but hard codes GMT as the time zone.
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" 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") var errTooLarge = errors.New("http: request too large")
// Read next request from connection. // Read next request from connection.
...@@ -620,27 +645,45 @@ func (w *response) WriteHeader(code int) { ...@@ -620,27 +645,45 @@ func (w *response) WriteHeader(code int) {
// the response Header map and all its 1-element slices. // the response Header map and all its 1-element slices.
type extraHeader struct { type extraHeader struct {
contentType string contentType string
contentLength string
connection string connection string
date string
transferEncoding string transferEncoding string
date []byte // written if not nil
contentLength []byte // written if not nil
} }
// Sorted the same as extraHeader.Write's loop. // Sorted the same as extraHeader.Write's loop.
var extraHeaderKeys = [][]byte{ var extraHeaderKeys = [][]byte{
[]byte("Content-Type"), []byte("Content-Length"), []byte("Content-Type"),
[]byte("Connection"), []byte("Date"), []byte("Transfer-Encoding"), []byte("Connection"),
[]byte("Transfer-Encoding"),
} }
// The value receiver, despite copying 5 strings to the stack, var (
// prevents an extra allocation. The escape analysis isn't smart headerContentLength = []byte("Content-Length: ")
// enough to realize this doesn't mutate h. headerDate = []byte("Date: ")
func (h extraHeader) Write(w io.Writer) { )
for i, v := range []string{h.contentType, h.contentLength, h.connection, h.date, h.transferEncoding} {
// 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 != "" { if v != "" {
w.Write(extraHeaderKeys[i]) w.Write(extraHeaderKeys[i])
w.Write(colonSpace) w.Write(colonSpace)
io.WriteString(w, v) w.WriteString(v)
w.Write(crlf) w.Write(crlf)
} }
} }
...@@ -694,7 +737,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -694,7 +737,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
// "keep-alive" connections alive. // "keep-alive" connections alive.
if w.handlerDone && 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))
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 // 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) { ...@@ -755,7 +798,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
} }
if _, ok := header["Date"]; !ok { 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") te := header.get("Transfer-Encoding")
...@@ -806,7 +849,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { ...@@ -806,7 +849,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
io.WriteString(w.conn.buf, statusLine(w.req, code)) io.WriteString(w.conn.buf, statusLine(w.req, code))
cw.header.WriteSubset(w.conn.buf, excludeHeader) cw.header.WriteSubset(w.conn.buf, excludeHeader)
setHeader.Write(w.conn.buf) setHeader.Write(w.conn.buf.Writer)
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