Commit 9d73a6dc authored by Justin Nuß's avatar Justin Nuß Committed by Brad Fitzpatrick

strconv: Avoid allocation in AppendQuote*

The current implementations of the AppendQuote functions use quoteWith
(through Quote) for quoting the given value and appends the returned
string to the dst byte slice. quoteWith internally creates a byte slice
on each call which gets converted to a string in Quote.

This means the AppendQuote functions always allocates a new byte slice
and a string only to append them to an existing byte slice. In the case
of (Append)QuoteRune the string passed to quoteWith will also needs to
be allocated from a rune first.

Split quoteWith into two functions (quoteWith and appendQuotedWith) and
replace the call to Quote inside AppendQuote with appendQuotedWith,
which appends directly to the byte slice passed to AppendQuote and also
avoids the []byte->string conversion.

Also introduce the 2 functions quoteRuneWith and appendQuotedRuneWith
that work the same way as quoteWith and appendQuotedWith, but take a
single rune instead of a string, to avoid allocating a new string when
appending a single rune, and use them in (Append)QuoteRune.

Also update the ToASCII and ToGraphic variants to use the new functions.

Benchmark results:

benchmark                      old ns/op     new ns/op     delta
BenchmarkQuote-8               428           503           +17.52%
BenchmarkQuoteRune-8           148           105           -29.05%
BenchmarkAppendQuote-8         435           307           -29.43%
BenchmarkAppendQuoteRune-8     158           23.5          -85.13%

benchmark                      old allocs     new allocs     delta
BenchmarkQuote-8               3              3              +0.00%
BenchmarkQuoteRune-8           3              2              -33.33%
BenchmarkAppendQuote-8         3              0              -100.00%
BenchmarkAppendQuoteRune-8     3              0              -100.00%

benchmark                      old bytes     new bytes     delta
BenchmarkQuote-8               144           144           +0.00%
BenchmarkQuoteRune-8           16            16            +0.00%
BenchmarkAppendQuote-8         144           0             -100.00%
BenchmarkAppendQuoteRune-8     16            0             -100.00%

Change-Id: I77c148d5c7242f1b0edbbeeea184878abb51a522
Reviewed-on: https://go-review.googlesource.com/18962
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
parent f3f920ff
...@@ -6,15 +6,19 @@ ...@@ -6,15 +6,19 @@
package strconv package strconv
import ( import "unicode/utf8"
"unicode/utf8"
)
const lowerhex = "0123456789abcdef" const lowerhex = "0123456789abcdef"
func quoteWith(s string, quote byte, ASCIIonly, graphicOnly bool) string { func quoteWith(s string, quote byte, ASCIIonly, graphicOnly bool) string {
var runeTmp [utf8.UTFMax]byte return string(appendQuotedWith(make([]byte, 0, 3*len(s)/2), s, quote, ASCIIonly, graphicOnly))
buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations. }
func quoteRuneWith(r rune, quote byte, ASCIIonly, graphicOnly bool) string {
return string(appendQuotedRuneWith(nil, r, quote, ASCIIonly, graphicOnly))
}
func appendQuotedWith(buf []byte, s string, quote byte, ASCIIonly, graphicOnly bool) []byte {
buf = append(buf, quote) buf = append(buf, quote)
for width := 0; len(s) > 0; s = s[width:] { for width := 0; len(s) > 0; s = s[width:] {
r := rune(s[0]) r := rune(s[0])
...@@ -28,61 +32,76 @@ func quoteWith(s string, quote byte, ASCIIonly, graphicOnly bool) string { ...@@ -28,61 +32,76 @@ func quoteWith(s string, quote byte, ASCIIonly, graphicOnly bool) string {
buf = append(buf, lowerhex[s[0]&0xF]) buf = append(buf, lowerhex[s[0]&0xF])
continue continue
} }
if r == rune(quote) || r == '\\' { // always backslashed buf = appendEscapedRune(buf, r, width, quote, ASCIIonly, graphicOnly)
buf = append(buf, '\\') }
buf = append(buf, quote)
return buf
}
func appendQuotedRuneWith(buf []byte, r rune, quote byte, ASCIIonly, graphicOnly bool) []byte {
buf = append(buf, quote)
if !utf8.ValidRune(r) {
r = utf8.RuneError
}
buf = appendEscapedRune(buf, r, utf8.RuneLen(r), quote, ASCIIonly, graphicOnly)
buf = append(buf, quote)
return buf
}
func appendEscapedRune(buf []byte, r rune, width int, quote byte, ASCIIonly, graphicOnly bool) []byte {
var runeTmp [utf8.UTFMax]byte
if r == rune(quote) || r == '\\' { // always backslashed
buf = append(buf, '\\')
buf = append(buf, byte(r))
return buf
}
if ASCIIonly {
if r < utf8.RuneSelf && IsPrint(r) {
buf = append(buf, byte(r)) buf = append(buf, byte(r))
continue return buf
} }
if ASCIIonly { } else if IsPrint(r) || graphicOnly && isInGraphicList(r) {
if r < utf8.RuneSelf && IsPrint(r) { n := utf8.EncodeRune(runeTmp[:], r)
buf = append(buf, byte(r)) buf = append(buf, runeTmp[:n]...)
continue return buf
}
switch r {
case '\a':
buf = append(buf, `\a`...)
case '\b':
buf = append(buf, `\b`...)
case '\f':
buf = append(buf, `\f`...)
case '\n':
buf = append(buf, `\n`...)
case '\r':
buf = append(buf, `\r`...)
case '\t':
buf = append(buf, `\t`...)
case '\v':
buf = append(buf, `\v`...)
default:
switch {
case r < ' ':
buf = append(buf, `\x`...)
buf = append(buf, lowerhex[byte(r)>>4])
buf = append(buf, lowerhex[byte(r)&0xF])
case r > utf8.MaxRune:
r = 0xFFFD
fallthrough
case r < 0x10000:
buf = append(buf, `\u`...)
for s := 12; s >= 0; s -= 4 {
buf = append(buf, lowerhex[r>>uint(s)&0xF])
} }
} else if IsPrint(r) || graphicOnly && isInGraphicList(r) {
n := utf8.EncodeRune(runeTmp[:], r)
buf = append(buf, runeTmp[:n]...)
continue
}
switch r {
case '\a':
buf = append(buf, `\a`...)
case '\b':
buf = append(buf, `\b`...)
case '\f':
buf = append(buf, `\f`...)
case '\n':
buf = append(buf, `\n`...)
case '\r':
buf = append(buf, `\r`...)
case '\t':
buf = append(buf, `\t`...)
case '\v':
buf = append(buf, `\v`...)
default: default:
switch { buf = append(buf, `\U`...)
case r < ' ': for s := 28; s >= 0; s -= 4 {
buf = append(buf, `\x`...) buf = append(buf, lowerhex[r>>uint(s)&0xF])
buf = append(buf, lowerhex[s[0]>>4])
buf = append(buf, lowerhex[s[0]&0xF])
case r > utf8.MaxRune:
r = 0xFFFD
fallthrough
case r < 0x10000:
buf = append(buf, `\u`...)
for s := 12; s >= 0; s -= 4 {
buf = append(buf, lowerhex[r>>uint(s)&0xF])
}
default:
buf = append(buf, `\U`...)
for s := 28; s >= 0; s -= 4 {
buf = append(buf, lowerhex[r>>uint(s)&0xF])
}
} }
} }
} }
buf = append(buf, quote) return buf
return string(buf)
} }
// Quote returns a double-quoted Go string literal representing s. The // Quote returns a double-quoted Go string literal representing s. The
...@@ -96,7 +115,7 @@ func Quote(s string) string { ...@@ -96,7 +115,7 @@ func Quote(s string) string {
// AppendQuote appends a double-quoted Go string literal representing s, // AppendQuote appends a double-quoted Go string literal representing s,
// as generated by Quote, to dst and returns the extended buffer. // as generated by Quote, to dst and returns the extended buffer.
func AppendQuote(dst []byte, s string) []byte { func AppendQuote(dst []byte, s string) []byte {
return append(dst, Quote(s)...) return appendQuotedWith(dst, s, '"', false, false)
} }
// QuoteToASCII returns a double-quoted Go string literal representing s. // QuoteToASCII returns a double-quoted Go string literal representing s.
...@@ -109,7 +128,7 @@ func QuoteToASCII(s string) string { ...@@ -109,7 +128,7 @@ func QuoteToASCII(s string) string {
// AppendQuoteToASCII appends a double-quoted Go string literal representing s, // AppendQuoteToASCII appends a double-quoted Go string literal representing s,
// as generated by QuoteToASCII, to dst and returns the extended buffer. // as generated by QuoteToASCII, to dst and returns the extended buffer.
func AppendQuoteToASCII(dst []byte, s string) []byte { func AppendQuoteToASCII(dst []byte, s string) []byte {
return append(dst, QuoteToASCII(s)...) return appendQuotedWith(dst, s, '"', true, false)
} }
// QuoteToGraphic returns a double-quoted Go string literal representing s. // QuoteToGraphic returns a double-quoted Go string literal representing s.
...@@ -122,21 +141,20 @@ func QuoteToGraphic(s string) string { ...@@ -122,21 +141,20 @@ func QuoteToGraphic(s string) string {
// AppendQuoteToGraphic appends a double-quoted Go string literal representing s, // AppendQuoteToGraphic appends a double-quoted Go string literal representing s,
// as generated by QuoteToGraphic, to dst and returns the extended buffer. // as generated by QuoteToGraphic, to dst and returns the extended buffer.
func AppendQuoteToGraphic(dst []byte, s string) []byte { func AppendQuoteToGraphic(dst []byte, s string) []byte {
return append(dst, QuoteToGraphic(s)...) return appendQuotedWith(dst, s, '"', false, true)
} }
// QuoteRune returns a single-quoted Go character literal representing the // QuoteRune returns a single-quoted Go character literal representing the
// rune. The returned string uses Go escape sequences (\t, \n, \xFF, \u0100) // rune. The returned string uses Go escape sequences (\t, \n, \xFF, \u0100)
// for control characters and non-printable characters as defined by IsPrint. // for control characters and non-printable characters as defined by IsPrint.
func QuoteRune(r rune) string { func QuoteRune(r rune) string {
// TODO: avoid the allocation here. return quoteRuneWith(r, '\'', false, false)
return quoteWith(string(r), '\'', false, false)
} }
// AppendQuoteRune appends a single-quoted Go character literal representing the rune, // AppendQuoteRune appends a single-quoted Go character literal representing the rune,
// as generated by QuoteRune, to dst and returns the extended buffer. // as generated by QuoteRune, to dst and returns the extended buffer.
func AppendQuoteRune(dst []byte, r rune) []byte { func AppendQuoteRune(dst []byte, r rune) []byte {
return append(dst, QuoteRune(r)...) return appendQuotedRuneWith(dst, r, '\'', false, false)
} }
// QuoteRuneToASCII returns a single-quoted Go character literal representing // QuoteRuneToASCII returns a single-quoted Go character literal representing
...@@ -144,14 +162,13 @@ func AppendQuoteRune(dst []byte, r rune) []byte { ...@@ -144,14 +162,13 @@ func AppendQuoteRune(dst []byte, r rune) []byte {
// \u0100) for non-ASCII characters and non-printable characters as defined // \u0100) for non-ASCII characters and non-printable characters as defined
// by IsPrint. // by IsPrint.
func QuoteRuneToASCII(r rune) string { func QuoteRuneToASCII(r rune) string {
// TODO: avoid the allocation here. return quoteRuneWith(r, '\'', true, false)
return quoteWith(string(r), '\'', true, false)
} }
// AppendQuoteRuneToASCII appends a single-quoted Go character literal representing the rune, // AppendQuoteRuneToASCII appends a single-quoted Go character literal representing the rune,
// as generated by QuoteRuneToASCII, to dst and returns the extended buffer. // as generated by QuoteRuneToASCII, to dst and returns the extended buffer.
func AppendQuoteRuneToASCII(dst []byte, r rune) []byte { func AppendQuoteRuneToASCII(dst []byte, r rune) []byte {
return append(dst, QuoteRuneToASCII(r)...) return appendQuotedRuneWith(dst, r, '\'', true, false)
} }
// QuoteRuneToGraphic returns a single-quoted Go character literal representing // QuoteRuneToGraphic returns a single-quoted Go character literal representing
...@@ -159,14 +176,13 @@ func AppendQuoteRuneToASCII(dst []byte, r rune) []byte { ...@@ -159,14 +176,13 @@ func AppendQuoteRuneToASCII(dst []byte, r rune) []byte {
// \u0100) for non-ASCII characters and non-printable characters as defined // \u0100) for non-ASCII characters and non-printable characters as defined
// by IsGraphic. // by IsGraphic.
func QuoteRuneToGraphic(r rune) string { func QuoteRuneToGraphic(r rune) string {
// TODO: avoid the allocation here. return quoteRuneWith(r, '\'', false, true)
return quoteWith(string(r), '\'', false, true)
} }
// AppendQuoteRuneToGraphic appends a single-quoted Go character literal representing the rune, // AppendQuoteRuneToGraphic appends a single-quoted Go character literal representing the rune,
// as generated by QuoteRuneToGraphic, to dst and returns the extended buffer. // as generated by QuoteRuneToGraphic, to dst and returns the extended buffer.
func AppendQuoteRuneToGraphic(dst []byte, r rune) []byte { func AppendQuoteRuneToGraphic(dst []byte, r rune) []byte {
return append(dst, QuoteRuneToGraphic(r)...) return appendQuotedRuneWith(dst, r, '\'', false, true)
} }
// CanBackquote reports whether the string s can be represented // CanBackquote reports whether the string s can be represented
......
...@@ -89,6 +89,34 @@ func TestQuoteToGraphic(t *testing.T) { ...@@ -89,6 +89,34 @@ func TestQuoteToGraphic(t *testing.T) {
} }
} }
func BenchmarkQuote(b *testing.B) {
for i := 0; i < b.N; i++ {
Quote("\a\b\f\r\n\t\v\a\b\f\r\n\t\v\a\b\f\r\n\t\v")
}
}
func BenchmarkQuoteRune(b *testing.B) {
for i := 0; i < b.N; i++ {
QuoteRune('\a')
}
}
var benchQuoteBuf []byte
func BenchmarkAppendQuote(b *testing.B) {
for i := 0; i < b.N; i++ {
benchQuoteBuf = AppendQuote(benchQuoteBuf[:0], "\a\b\f\r\n\t\v\a\b\f\r\n\t\v\a\b\f\r\n\t\v")
}
}
var benchQuoteRuneBuf []byte
func BenchmarkAppendQuoteRune(b *testing.B) {
for i := 0; i < b.N; i++ {
benchQuoteRuneBuf = AppendQuoteRune(benchQuoteRuneBuf[:0], '\a')
}
}
type quoteRuneTest struct { type quoteRuneTest struct {
in rune in rune
out string out string
......
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