Skip to content

Commit 7e2aa7f

Browse files
committed
Expose AppendJSONString() function for fast construction of JSON-encoded 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
1 parent a0b6a21 commit 7e2aa7f

File tree

4 files changed

+109
-28
lines changed

4 files changed

+109
-28
lines changed

jsonstring.go

+58-23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package quicktemplate
22

33
import (
4+
"bytes"
45
"fmt"
56
"strings"
67
)
@@ -17,7 +18,10 @@ func hasSpecialChars(s string) bool {
1718
return false
1819
}
1920

20-
func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
21+
// AppendJSONString appends json-encoded string s to dst and returns the result.
22+
//
23+
// If addQuotes is true, then the appended json string is wrapped into double quotes.
24+
func AppendJSONString(dst []byte, s string, addQuotes bool) []byte {
2125
if !hasSpecialChars(s) {
2226
// Fast path - nothing to escape.
2327
if !addQuotes {
@@ -33,33 +37,64 @@ func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
3337
if addQuotes {
3438
dst = append(dst, '"')
3539
}
36-
bb := AcquireByteBuffer()
37-
var tmp []byte
38-
tmp, bb.B = bb.B, dst
39-
_, err := jsonReplacer.WriteString(bb, s)
40-
if err != nil {
41-
panic(fmt.Errorf("BUG: unexpected error returned from jsonReplacer.WriteString: %s", err))
42-
}
43-
dst, bb.B = bb.B, tmp
44-
ReleaseByteBuffer(bb)
40+
dst = jsonReplacer.AppendReplace(dst, s)
4541
if addQuotes {
4642
dst = append(dst, '"')
4743
}
4844
return dst
4945
}
5046

51-
var jsonReplacer = strings.NewReplacer(func() []string {
52-
a := []string{
53-
"\n", `\n`,
54-
"\r", `\r`,
55-
"\t", `\t`,
56-
"\"", `\"`,
57-
"\\", `\\`,
58-
"<", `\u003c`,
59-
"'", `\u0027`,
60-
}
47+
var jsonReplacer = newByteReplacer(func() ([]byte, []string) {
48+
oldChars := []byte("\n\r\t\b\f\"\\<'")
49+
newStrings := []string{`\n`, `\r`, `\t`, `\b`, `\f`, `\"`, `\\`, `\u003c`, `\u0027`}
6150
for i := 0; i < 0x20; i++ {
62-
a = append(a, string([]byte{byte(i)}), fmt.Sprintf(`\u%04x`, i))
51+
c := byte(i)
52+
if n := bytes.IndexByte(oldChars, c); n >= 0 {
53+
continue
54+
}
55+
oldChars = append(oldChars, byte(i))
56+
newStrings = append(newStrings, fmt.Sprintf(`\u%04x`, i))
57+
}
58+
return oldChars, newStrings
59+
}())
60+
61+
type byteReplacer struct {
62+
m [256]byte
63+
newStrings []string
64+
}
65+
66+
func newByteReplacer(oldChars []byte, newStrings []string) *byteReplacer {
67+
if len(oldChars) != len(newStrings) {
68+
panic(fmt.Errorf("len(oldChars)=%d must be equal to len(newStrings)=%d", len(oldChars), len(newStrings)))
69+
}
70+
if len(oldChars) >= 255 {
71+
panic(fmt.Errorf("len(oldChars)=%d must be smaller than 255", len(oldChars)))
72+
}
73+
74+
var m [256]byte
75+
for i := range m[:] {
76+
m[i] = 255
6377
}
64-
return a
65-
}()...)
78+
for i, c := range oldChars {
79+
m[c] = byte(i)
80+
}
81+
return &byteReplacer{
82+
m: m,
83+
newStrings: newStrings,
84+
}
85+
}
86+
87+
func (br *byteReplacer) AppendReplace(dst []byte, s string) []byte {
88+
m := br.m
89+
newStrings := br.newStrings
90+
for i := 0; i < len(s); i++ {
91+
c := s[i]
92+
n := m[c]
93+
if n == 255 {
94+
dst = append(dst, c)
95+
} else {
96+
dst = append(dst, newStrings[n]...)
97+
}
98+
}
99+
return dst
100+
}

jsonstring_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func testAppendJSONString(t *testing.T, s string) {
3131
expectedResult = expectedResult[1 : len(expectedResult)-1]
3232

3333
bb := AcquireByteBuffer()
34-
bb.B = appendJSONString(bb.B[:0], s, false)
34+
bb.B = AppendJSONString(bb.B[:0], s, false)
3535
result := string(bb.B)
3636
ReleaseByteBuffer(bb)
3737

jsonstring_timing_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package quicktemplate
2+
3+
import (
4+
"strconv"
5+
"testing"
6+
)
7+
8+
func BenchmarkAppendJSONString(b *testing.B) {
9+
b.Run("no-special-chars", func(b *testing.B) {
10+
benchmarkAppendJSONString(b, "foo bar baz abc defkjlkj lkjdfs klsdjflfdjoqjo lkj ss")
11+
})
12+
b.Run("with-special-chars", func(b *testing.B) {
13+
benchmarkAppendJSONString(b, `foo bar baz abc defkjlkj lkjdf" klsdjflfdjoqjo\lkj ss`)
14+
})
15+
}
16+
17+
func benchmarkAppendJSONString(b *testing.B, s string) {
18+
b.ReportAllocs()
19+
b.SetBytes(int64(len(s)))
20+
b.RunParallel(func(pb *testing.PB) {
21+
var buf []byte
22+
for pb.Next() {
23+
buf = AppendJSONString(buf[:0], s, true)
24+
}
25+
})
26+
}
27+
28+
func BenchmarkAppendJSONStringViaStrconv(b *testing.B) {
29+
b.Run("no-special-chars", func(b *testing.B) {
30+
benchmarkAppendJSONStringViaStrconv(b, "foo bar baz abc defkjlkj lkjdfs klsdjflfdjoqjo lkj ss")
31+
})
32+
b.Run("with-special-chars", func(b *testing.B) {
33+
benchmarkAppendJSONStringViaStrconv(b, `foo bar baz abc defkjlkj lkjdf" klsdjflfdjoqjo\lkj ss`)
34+
})
35+
}
36+
37+
func benchmarkAppendJSONStringViaStrconv(b *testing.B, s string) {
38+
b.ReportAllocs()
39+
b.SetBytes(int64(len(s)))
40+
b.RunParallel(func(pb *testing.PB) {
41+
var buf []byte
42+
for pb.Next() {
43+
buf = strconv.AppendQuote(buf[:0], s)
44+
}
45+
})
46+
}

writer.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,9 @@ func (w *QWriter) FPrec(f float64, prec int) {
164164
func (w *QWriter) Q(s string) {
165165
bb, ok := w.w.(*ByteBuffer)
166166
if ok {
167-
bb.B = appendJSONString(bb.B, s, true)
167+
bb.B = AppendJSONString(bb.B, s, true)
168168
} else {
169-
w.b = appendJSONString(w.b[:0], s, true)
169+
w.b = AppendJSONString(w.b[:0], s, true)
170170
w.Write(w.b)
171171
}
172172
}
@@ -184,9 +184,9 @@ func (w *QWriter) QZ(z []byte) {
184184
func (w *QWriter) J(s string) {
185185
bb, ok := w.w.(*ByteBuffer)
186186
if ok {
187-
bb.B = appendJSONString(bb.B, s, false)
187+
bb.B = AppendJSONString(bb.B, s, false)
188188
} else {
189-
w.b = appendJSONString(w.b[:0], s, false)
189+
w.b = AppendJSONString(w.b[:0], s, false)
190190
w.Write(w.b)
191191
}
192192
}

0 commit comments

Comments
 (0)