From 36ba831347092bfa941c4c8afa6ffd8bb3477ef8 Mon Sep 17 00:00:00 2001 From: uwaterloo gitlab Date: Sun, 24 Oct 2021 07:09:24 -0400 Subject: [PATCH] Adding zero-allocation uint64 to byte slice conversion and fixing the ResponseHeader.SetStatusLine function call signature --- bytesconv.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ header.go | 8 +++--- header_test.go | 6 ++--- http_test.go | 2 +- status.go | 15 +++++++++-- 5 files changed, 90 insertions(+), 10 deletions(-) diff --git a/bytesconv.go b/bytesconv.go index 81f8e5b167..4ee193d4b2 100644 --- a/bytesconv.go +++ b/bytesconv.go @@ -350,6 +350,75 @@ func s2b(s string) (b []byte) { return b } +const host32bit = ^uint(0)>>32 == 0 + +const smallsString = "00010203040506070809" + + "10111213141516171819" + + "20212223242526272829" + + "30313233343536373839" + + "40414243444546474849" + + "50515253545556575859" + + "60616263646566676869" + + "70717273747576777879" + + "80818283848586878889" + + "90919293949596979899" + +// i2b converts a uint64 to a byte slice without memory allocation. +// +// Note that this is a slightly modified version of strconv.formatBits +func i2b(dst []byte, i uint64) []byte { + var a [64 + 1]byte + ignore := len(a) + + // convert bits + // We use uint values where we can because those will + // fit into a single register even on a 32bit machine. + if host32bit { + // convert the lower digits using 32bit operations + for i >= 1e9 { + // Avoid using r = a%b in addition to q = a/b + // since 64bit division and modulo operations + // are calculated by runtime functions on 32bit machines. + q := i / 1e9 + is := uint(i - q*1e9) // i % 1e9 fits into a uint + for j := 4; j > 0; j-- { + is := is % 100 * 2 + is /= 100 + ignore -= 2 + a[ignore+1] = smallsString[is+1] + a[ignore+0] = smallsString[is+0] + } + + // is < 10, since it contains the last digit + // from the initial 9-digit is. + ignore-- + a[ignore] = smallsString[is*2+1] + + i = q + } + // i < 1e9 + } + // i guaranteed to fit into a uint + us := uint(i) + for us >= 100 { + is := us % 100 * 2 + us /= 100 + ignore -= 2 + a[ignore+1] = smallsString[is+1] + a[ignore+0] = smallsString[is+0] + } + + // us < 100 + is := us * 2 + ignore-- + a[ignore] = smallsString[is+1] + if us >= 10 { + ignore-- + a[ignore] = smallsString[is] + } + return append(dst, a[ignore:]...) +} + // AppendUnquotedArg appends url-decoded src to dst and returns appended dst. // // dst may point to src. In this case src will be overwritten. diff --git a/header.go b/header.go index 4cbe9f3832..4c31bc7db4 100644 --- a/header.go +++ b/header.go @@ -146,8 +146,8 @@ func (h *ResponseHeader) StatusLine() []byte { } // SetStatusLine sets response status line bytes. -func (h *ResponseHeader) SetStatusLine(statusLine []byte) { - h.statusLine = append(h.statusLine[:0], statusLine...) +func (h *ResponseHeader) SetStatusLine(statusCode int, statusLine []byte) { + h.statusLine = formatStatusLine(h.statusLine, statusCode, statusLine) } // SetLastModified sets 'Last-Modified' header to the given value. @@ -1880,8 +1880,8 @@ func (h *ResponseHeader) parseFirstLine(buf []byte) (int, error) { } return 0, fmt.Errorf("unexpected char at the end of status code. Response %q", buf) } - if len(b) > n+1 && !bytes.Equal(b[n+1:], statusLine(h.statusCode)) { - h.SetStatusLine(b[n+1:]) + if len(b) > n+1 && !bytes.Equal(b[n+1:], statusLines[h.statusCode]) { + h.SetStatusLine(h.statusCode, b[n+1:]) } return len(buf) - len(bNext), nil diff --git a/header_test.go b/header_test.go index 19138ad70c..6dac8339ea 100644 --- a/header_test.go +++ b/header_test.go @@ -52,8 +52,8 @@ func TestResponseHeaderMultiLineValue(t *testing.T) { t.Fatalf("parse response using net/http failed, %s", err) } - if !bytes.Equal(header.StatusLine(), []byte("SuperOK")) { - t.Errorf("parse status line with non-default value failed, got: %s want: SuperOK", header.StatusLine()) + if !bytes.Equal(header.StatusLine(), []byte("HTTP/1.1 200 SuperOK\r\n")) { + t.Errorf("parse status line with non-default value failed, got: %s want: HTTP/1.1 200 SuperOK", header.StatusLine()) } for name, vals := range response.Header { @@ -83,7 +83,7 @@ func TestResponseHeaderMultiLineName(t *testing.T) { t.Errorf("expected error, got %q (%v)", m, err) } - if !bytes.Equal(header.StatusLine(), []byte("OK")) { + if !bytes.Equal(header.StatusLine(), []byte("HTTP/1.1 200 OK\r\n")) { t.Errorf("expected default status line, got: %s", header.StatusLine()) } } diff --git a/http_test.go b/http_test.go index 8d57bb050a..a5af571f73 100644 --- a/http_test.go +++ b/http_test.go @@ -839,7 +839,7 @@ func TestResponseSkipBody(t *testing.T) { // set StatusNoContent with statusLine r.Header.SetStatusCode(StatusNoContent) - r.Header.SetStatusLine([]byte("HTTP/1.1 204 NC\r\n")) + r.Header.SetStatusLine(204, []byte("NC")) r.SetBodyString("foobar") s = r.String() if strings.Contains(s, "\r\n\r\nfoobar") { diff --git a/status.go b/status.go index 28d1286e0b..381e9b1f8c 100644 --- a/status.go +++ b/status.go @@ -1,7 +1,6 @@ package fasthttp import ( - "fmt" "strconv" ) @@ -81,6 +80,8 @@ const ( ) var ( + httpHeader = []byte("HTTP/1.1") + statusLines = make([][]byte, statusMessageMax+1) statusMessages = []string{ @@ -168,10 +169,20 @@ func StatusMessage(statusCode int) string { func init() { // Fill all valid status lines for i := 0; i < len(statusLines); i++ { - statusLines[i] = []byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", i, StatusMessage(i))) + statusLines[i] = formatStatusLine([]byte{}, i, []byte(StatusMessage(i))) } } +func formatStatusLine(dst []byte, statusCode int, statusMessage []byte) []byte { + dst = append(dst[:0], httpHeader...) + dst = append(dst, ' ') + dst = i2b(dst, uint64(statusCode)) + dst = append(dst, ' ') + dst = append(dst, statusMessage...) + dst = append(dst, strCRLF...) + return dst +} + func statusLine(statusCode int) []byte { if statusCode < 0 || statusCode > statusMessageMax { return invalidStatusLine(statusCode)