Skip to content

Commit

Permalink
Expose AppendJSONString() function for fast construction of JSON-enco…
Browse files Browse the repository at this point in the history
…ded strings

While at it, optimize AppendJSONString() for cases when the string contains chars, which need to be escaped

Benchmark results:

BenchmarkAppendJSONString/no-special-chars-16         	134810218	         8.647 ns/op	6129.48 MB/s	       0 B/op	       0 allocs/op
BenchmarkAppendJSONString/with-special-chars-16       	72410246	        17.31 ns/op	3061.36 MB/s	       0 B/op	       0 allocs/op
BenchmarkAppendJSONStringViaStrconv/no-special-chars-16         	14370494	        78.87 ns/op	 672.03 MB/s	       0 B/op	       0 allocs/op
BenchmarkAppendJSONStringViaStrconv/with-special-chars-16       	14202278	        81.71 ns/op	 648.65 MB/s	       0 B/op	       0 allocs/op
  • Loading branch information
valyala committed Jul 4, 2024
1 parent a0b6a21 commit 7e2aa7f
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 28 deletions.
81 changes: 58 additions & 23 deletions jsonstring.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package quicktemplate

import (
"bytes"
"fmt"
"strings"
)
Expand All @@ -17,7 +18,10 @@ func hasSpecialChars(s string) bool {
return false
}

func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
// AppendJSONString appends json-encoded string s to dst and returns the result.
//
// If addQuotes is true, then the appended json string is wrapped into double quotes.
func AppendJSONString(dst []byte, s string, addQuotes bool) []byte {
if !hasSpecialChars(s) {
// Fast path - nothing to escape.
if !addQuotes {
Expand All @@ -33,33 +37,64 @@ func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
if addQuotes {
dst = append(dst, '"')
}
bb := AcquireByteBuffer()
var tmp []byte
tmp, bb.B = bb.B, dst
_, err := jsonReplacer.WriteString(bb, s)
if err != nil {
panic(fmt.Errorf("BUG: unexpected error returned from jsonReplacer.WriteString: %s", err))
}
dst, bb.B = bb.B, tmp
ReleaseByteBuffer(bb)
dst = jsonReplacer.AppendReplace(dst, s)
if addQuotes {
dst = append(dst, '"')
}
return dst
}

var jsonReplacer = strings.NewReplacer(func() []string {
a := []string{
"\n", `\n`,
"\r", `\r`,
"\t", `\t`,
"\"", `\"`,
"\\", `\\`,
"<", `\u003c`,
"'", `\u0027`,
}
var jsonReplacer = newByteReplacer(func() ([]byte, []string) {
oldChars := []byte("\n\r\t\b\f\"\\<'")
newStrings := []string{`\n`, `\r`, `\t`, `\b`, `\f`, `\"`, `\\`, `\u003c`, `\u0027`}
for i := 0; i < 0x20; i++ {
a = append(a, string([]byte{byte(i)}), fmt.Sprintf(`\u%04x`, i))
c := byte(i)
if n := bytes.IndexByte(oldChars, c); n >= 0 {
continue
}
oldChars = append(oldChars, byte(i))
newStrings = append(newStrings, fmt.Sprintf(`\u%04x`, i))
}
return oldChars, newStrings
}())

type byteReplacer struct {
m [256]byte
newStrings []string
}

func newByteReplacer(oldChars []byte, newStrings []string) *byteReplacer {
if len(oldChars) != len(newStrings) {
panic(fmt.Errorf("len(oldChars)=%d must be equal to len(newStrings)=%d", len(oldChars), len(newStrings)))
}
if len(oldChars) >= 255 {
panic(fmt.Errorf("len(oldChars)=%d must be smaller than 255", len(oldChars)))
}

var m [256]byte
for i := range m[:] {
m[i] = 255
}
return a
}()...)
for i, c := range oldChars {
m[c] = byte(i)
}
return &byteReplacer{
m: m,
newStrings: newStrings,
}
}

func (br *byteReplacer) AppendReplace(dst []byte, s string) []byte {
m := br.m
newStrings := br.newStrings
for i := 0; i < len(s); i++ {
c := s[i]
n := m[c]
if n == 255 {
dst = append(dst, c)
} else {
dst = append(dst, newStrings[n]...)
}
}
return dst
}
2 changes: 1 addition & 1 deletion jsonstring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func testAppendJSONString(t *testing.T, s string) {
expectedResult = expectedResult[1 : len(expectedResult)-1]

bb := AcquireByteBuffer()
bb.B = appendJSONString(bb.B[:0], s, false)
bb.B = AppendJSONString(bb.B[:0], s, false)
result := string(bb.B)
ReleaseByteBuffer(bb)

Expand Down
46 changes: 46 additions & 0 deletions jsonstring_timing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package quicktemplate

import (
"strconv"
"testing"
)

func BenchmarkAppendJSONString(b *testing.B) {
b.Run("no-special-chars", func(b *testing.B) {
benchmarkAppendJSONString(b, "foo bar baz abc defkjlkj lkjdfs klsdjflfdjoqjo lkj ss")
})
b.Run("with-special-chars", func(b *testing.B) {
benchmarkAppendJSONString(b, `foo bar baz abc defkjlkj lkjdf" klsdjflfdjoqjo\lkj ss`)
})
}

func benchmarkAppendJSONString(b *testing.B, s string) {
b.ReportAllocs()
b.SetBytes(int64(len(s)))
b.RunParallel(func(pb *testing.PB) {
var buf []byte
for pb.Next() {
buf = AppendJSONString(buf[:0], s, true)
}
})
}

func BenchmarkAppendJSONStringViaStrconv(b *testing.B) {
b.Run("no-special-chars", func(b *testing.B) {
benchmarkAppendJSONStringViaStrconv(b, "foo bar baz abc defkjlkj lkjdfs klsdjflfdjoqjo lkj ss")
})
b.Run("with-special-chars", func(b *testing.B) {
benchmarkAppendJSONStringViaStrconv(b, `foo bar baz abc defkjlkj lkjdf" klsdjflfdjoqjo\lkj ss`)
})
}

func benchmarkAppendJSONStringViaStrconv(b *testing.B, s string) {
b.ReportAllocs()
b.SetBytes(int64(len(s)))
b.RunParallel(func(pb *testing.PB) {
var buf []byte
for pb.Next() {
buf = strconv.AppendQuote(buf[:0], s)
}
})
}
8 changes: 4 additions & 4 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@ func (w *QWriter) FPrec(f float64, prec int) {
func (w *QWriter) Q(s string) {
bb, ok := w.w.(*ByteBuffer)
if ok {
bb.B = appendJSONString(bb.B, s, true)
bb.B = AppendJSONString(bb.B, s, true)
} else {
w.b = appendJSONString(w.b[:0], s, true)
w.b = AppendJSONString(w.b[:0], s, true)
w.Write(w.b)
}
}
Expand All @@ -184,9 +184,9 @@ func (w *QWriter) QZ(z []byte) {
func (w *QWriter) J(s string) {
bb, ok := w.w.(*ByteBuffer)
if ok {
bb.B = appendJSONString(bb.B, s, false)
bb.B = AppendJSONString(bb.B, s, false)
} else {
w.b = appendJSONString(w.b[:0], s, false)
w.b = AppendJSONString(w.b[:0], s, false)
w.Write(w.b)
}
}
Expand Down

0 comments on commit 7e2aa7f

Please sign in to comment.