Skip to content

Commit e885dec

Browse files
authored
Faster constant compares and numbers (#50)
- Faster stage 2 parsing - Faster number parsing ``` benchmark old ns/op new ns/op delta BenchmarkApache_builds/copy-32 138493 138857 +0.26% BenchmarkApache_builds/nocopy-32 105770 100223 -5.24% BenchmarkCanada/copy-32 11324695 11046309 -2.46% BenchmarkCanada/nocopy-32 11322561 11141677 -1.60% BenchmarkCitm_catalog/copy-32 1560859 1387570 -11.10% BenchmarkCitm_catalog/nocopy-32 1202324 1244282 +3.49% BenchmarkGithub_events/copy-32 77414 70334 -9.15% BenchmarkGithub_events/nocopy-32 62410 58026 -7.02% BenchmarkGsoc_2018/copy-32 1259296 1240525 -1.49% BenchmarkGsoc_2018/nocopy-32 1020954 1028086 +0.70% BenchmarkInstruments/copy-32 336638 285133 -15.30% BenchmarkInstruments/nocopy-32 258841 253191 -2.18% BenchmarkMarine_ik/copy-32 12680069 12667499 -0.10% BenchmarkMarine_ik/nocopy-32 12128601 12184505 +0.46% BenchmarkMesh/copy-32 3785664 3736734 -1.29% BenchmarkMesh/nocopy-32 3778827 3691958 -2.30% BenchmarkMesh_pretty/copy-32 4296619 4200696 -2.23% BenchmarkMesh_pretty/nocopy-32 4293104 4224935 -1.59% BenchmarkNumbers/copy-32 724006 720850 -0.44% BenchmarkNumbers/nocopy-32 723446 714401 -1.25% BenchmarkRandom/copy-32 1069045 974317 -8.86% BenchmarkRandom/nocopy-32 725070 720869 -0.58% BenchmarkTwitter/copy-32 621589 586054 -5.72% BenchmarkTwitter/nocopy-32 467495 450845 -3.56% BenchmarkTwitterescaped/copy-32 998509 944640 -5.39% BenchmarkTwitterescaped/nocopy-32 861515 852544 -1.04% BenchmarkUpdate_center/copy-32 713592 702087 -1.61% BenchmarkUpdate_center/nocopy-32 507995 510048 +0.40% ```
1 parent cc3ba10 commit e885dec

File tree

5 files changed

+71
-65
lines changed

5 files changed

+71
-65
lines changed

parse_json_amd64_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ func TestParseNumber(t *testing.T) {
233233
}
234234

235235
for _, tc := range testCases {
236-
tag, val, flags := parseNumber([]byte(fmt.Sprintf(`%s:`, tc.input)))
236+
id, val := parseNumber([]byte(fmt.Sprintf(`%s:`, tc.input)))
237+
tag := Tag(id >> JSONTAGOFFSET)
238+
flags := id & JSONVALUEMASK
237239
if tag != tc.wantTag {
238240
t.Errorf("TestParseNumber: got: %v want: %v", tag, tc.wantTag)
239241
}
@@ -304,7 +306,8 @@ func TestParseInt64(t *testing.T) {
304306
test := &parseInt64Tests[i]
305307
t.Run(test.in, func(t *testing.T) {
306308

307-
tag, val, _ := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in)))
309+
id, val := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in)))
310+
tag := Tag(id >> JSONTAGOFFSET)
308311
if tag != test.tag {
309312
// Ignore intentionally bad syntactical errors
310313
t.Errorf("TestParseInt64: got: %v want: %v", tag, test.tag)
@@ -487,7 +490,8 @@ func TestParseFloat64(t *testing.T) {
487490
for i := 0; i < len(atoftests); i++ {
488491
test := &atoftests[i]
489492
t.Run(test.in, func(t *testing.T) {
490-
tag, val, _ := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in)))
493+
id, val := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in)))
494+
tag := Tag(id >> JSONTAGOFFSET)
491495
switch tag {
492496
case TagEnd:
493497
if test.err == nil {

parse_number_amd64.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,75 +66,76 @@ var isNumberRune = [256]uint8{
6666
// parseNumber will parse the number starting in the buffer.
6767
// Any non-number characters at the end will be ignored.
6868
// Returns TagEnd if no valid value found be found.
69-
func parseNumber(buf []byte) (tag Tag, val, flags uint64) {
69+
func parseNumber(buf []byte) (id, val uint64) {
7070
pos := 0
7171
found := uint8(0)
7272
for i, v := range buf {
7373
t := isNumberRune[v]
7474
if t == 0 {
7575
//fmt.Println("aborting on", string(v), "in", string(buf[:i]))
76-
return TagEnd, 0, 0
76+
return 0, 0
7777
}
7878
if t == isEOVFlag {
7979
break
8080
}
8181
if t&isMustHaveDigitNext > 0 {
8282
// A period and minus must be followed by a digit
8383
if len(buf) < i+2 || isNumberRune[buf[i+1]]&isDigitFlag == 0 {
84-
return TagEnd, 0, 0
84+
return 0, 0
8585
}
8686
}
8787
found |= t
8888
pos = i + 1
8989
}
9090
if pos == 0 {
91-
return TagEnd, 0, 0
91+
return 0, 0
9292
}
9393
const maxIntLen = 20
94+
floatTag := uint64(TagFloat) << JSONTAGOFFSET
9495

9596
// Only try integers if we didn't find any float exclusive and it can fit in an integer.
9697
if found&isFloatOnlyFlag == 0 && pos <= maxIntLen {
9798
if found&isMinusFlag == 0 {
9899
if pos > 1 && buf[0] == '0' {
99100
// Integers cannot have a leading zero.
100-
return TagEnd, 0, 0
101+
return 0, 0
101102
}
102103
} else {
103104
if pos > 2 && buf[1] == '0' {
104105
// Integers cannot have a leading zero after minus.
105-
return TagEnd, 0, 0
106+
return 0, 0
106107
}
107108
}
108109
i64, err := strconv.ParseInt(unsafeBytesToString(buf[:pos]), 10, 64)
109110
if err == nil {
110-
return TagInteger, uint64(i64), 0
111+
return uint64(TagInteger) << JSONTAGOFFSET, uint64(i64)
111112
}
112113
if errors.Is(err, strconv.ErrRange) {
113-
flags |= uint64(FloatOverflowedInteger)
114+
floatTag |= uint64(FloatOverflowedInteger)
114115
}
115116

116117
if found&isMinusFlag == 0 {
117118
u64, err := strconv.ParseUint(unsafeBytesToString(buf[:pos]), 10, 64)
118119
if err == nil {
119-
return TagUint, u64, 0
120+
return uint64(TagUint) << JSONTAGOFFSET, u64
120121
}
121122
if errors.Is(err, strconv.ErrRange) {
122-
flags |= uint64(FloatOverflowedInteger)
123+
floatTag |= uint64(FloatOverflowedInteger)
123124
}
124125
}
125126
} else if found&isFloatOnlyFlag == 0 {
126-
flags |= uint64(FloatOverflowedInteger)
127+
floatTag |= uint64(FloatOverflowedInteger)
127128
}
128129

129130
if pos > 1 && buf[0] == '0' && isNumberRune[buf[1]]&isFloatOnlyFlag == 0 {
130131
// Float can only have have a leading 0 when followed by a period.
131-
return TagEnd, 0, 0
132+
return 0, 0
132133
}
133134
f64, err := strconv.ParseFloat(unsafeBytesToString(buf[:pos]), 64)
134135
if err == nil {
135-
return TagFloat, math.Float64bits(f64), flags
136+
return floatTag, math.Float64bits(f64)
136137
}
137-
return TagEnd, 0, 0
138+
return 0, 0
138139
}
139140

140141
// unsafeBytesToString should only be used when we have control of b.

parse_number_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ func TestNumberIsValid(t *testing.T) {
3131
// From: https://stackoverflow.com/a/13340826
3232
var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`)
3333
isValidNumber := func(s string) bool {
34-
tag, _, _ := parseNumber([]byte(s))
35-
return tag != TagEnd
34+
tag, _ := parseNumber([]byte(s))
35+
return tag != 0
3636
}
3737
validTests := []string{
3838
"0",

parsed_json.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -962,8 +962,8 @@ func (pj *ParsedJson) writeTapeTagVal(tag Tag, val uint64) {
962962
pj.Tape = append(pj.Tape, uint64(tag)<<56, val)
963963
}
964964

965-
func (pj *ParsedJson) writeTapeTagValFlags(tag Tag, val, flags uint64) {
966-
pj.Tape = append(pj.Tape, uint64(tag)<<56|flags, val)
965+
func (pj *ParsedJson) writeTapeTagValFlags(id, val uint64) {
966+
pj.Tape = append(pj.Tape, id, val)
967967
}
968968

969969
func (pj *ParsedJson) write_tape_s64(val int64) {

stage2_build_tape_amd64.go

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -115,52 +115,46 @@ func parseString(pj *ParsedJson, idx uint64, maxStringSize uint64, needCopy bool
115115
}
116116

117117
func addNumber(buf []byte, pj *ParsedJson) bool {
118-
tag, val, flags := parseNumber(buf)
119-
if tag == TagEnd {
118+
tag, val := parseNumber(buf)
119+
if tag == 0 {
120120
return false
121121
}
122-
pj.writeTapeTagValFlags(tag, val, flags)
122+
pj.writeTapeTagValFlags(tag, val)
123123
return true
124124
}
125125

126126
func isValidTrueAtom(buf []byte) bool {
127-
if len(buf) >= 8 { // fast path when there is enough space left in the buffer
128-
tv := uint64(0x0000000065757274) // "true "
129-
mask4 := uint64(0x00000000ffffffff)
130-
locval := binary.LittleEndian.Uint64(buf)
131-
error := (locval & mask4) ^ tv
132-
error |= uint64(isNotStructuralOrWhitespace(buf[4]))
133-
return error == 0
134-
} else if len(buf) >= 5 {
135-
return bytes.Compare(buf[:4], []byte("true")) == 0 && isNotStructuralOrWhitespace(buf[4]) == 0
127+
if len(buf) >= 5 { // fast path when there is enough space left in the buffer
128+
const tv = uint32(0x0000000065757274) // "true "
129+
locval := binary.LittleEndian.Uint32(buf)
130+
if locval == tv {
131+
return isNotStructuralOrWhitespace(buf[4]) == 0
132+
}
136133
}
137134
return false
138135
}
139136

140137
func isValidFalseAtom(buf []byte) bool {
141138
if len(buf) >= 8 { // fast path when there is enough space left in the buffer
142-
fv := uint64(0x00000065736c6166) // "false "
143-
mask5 := uint64(0x000000ffffffffff)
139+
const fv = uint64(0x00000065736c6166) // "false "
140+
const mask5 = uint64(0x000000ffffffffff)
141+
error := uint64(isNotStructuralOrWhitespace(buf[5]))
144142
locval := binary.LittleEndian.Uint64(buf)
145-
error := (locval & mask5) ^ fv
146-
error |= uint64(isNotStructuralOrWhitespace(buf[5]))
143+
error |= (locval & mask5) ^ fv
147144
return error == 0
148145
} else if len(buf) >= 6 {
149-
return bytes.Compare(buf[:5], []byte("false")) == 0 && isNotStructuralOrWhitespace(buf[5]) == 0
146+
return bytes.Equal(buf[:5], []byte("false")) && isNotStructuralOrWhitespace(buf[5]) == 0
150147
}
151148
return false
152149
}
153150

154151
func isValidNullAtom(buf []byte) bool {
155-
if len(buf) >= 8 { // fast path when there is enough space left in the buffer
156-
nv := uint64(0x000000006c6c756e) // "null "
157-
mask4 := uint64(0x00000000ffffffff)
158-
locval := binary.LittleEndian.Uint64(buf) // we want to avoid unaligned 64-bit loads (undefined in C/C++)
159-
error := (locval & mask4) ^ nv
160-
error |= uint64(isNotStructuralOrWhitespace(buf[4]))
161-
return error == 0
162-
} else if len(buf) >= 5 {
163-
return bytes.Compare(buf[:4], []byte("null")) == 0 && isNotStructuralOrWhitespace(buf[4]) == 0
152+
if len(buf) >= 5 { // fast path when there is enough space left in the buffer
153+
const nv = 0x000000006c6c756e // "null "
154+
locval := binary.LittleEndian.Uint32(buf) // we want to avoid unaligned 64-bit loads (undefined in C/C++)
155+
if locval == nv {
156+
return isNotStructuralOrWhitespace(buf[4]) == 0
157+
}
164158
}
165159
return false
166160
}
@@ -186,11 +180,11 @@ continueRoot:
186180
switch buf[idx] {
187181
case '{':
188182
pj.containingScopeOffset = append(pj.containingScopeOffset, (pj.get_current_loc()<<retAddressShift)|retAddressStartConst)
189-
pj.write_tape(0, buf[idx])
183+
pj.write_tape(0, '{')
190184
goto object_begin
191185
case '[':
192186
pj.containingScopeOffset = append(pj.containingScopeOffset, (pj.get_current_loc()<<retAddressShift)|retAddressStartConst)
193-
pj.write_tape(0, buf[idx])
187+
pj.write_tape(0, '[')
194188
goto arrayBegin
195189
default:
196190
goto fail
@@ -267,24 +261,19 @@ object_key_state:
267261
if !isValidTrueAtom(buf[idx:]) {
268262
goto fail
269263
}
270-
pj.write_tape(0, buf[idx])
264+
pj.write_tape(0, 't')
271265

272266
case 'f':
273267
if !isValidFalseAtom(buf[idx:]) {
274268
goto fail
275269
}
276-
pj.write_tape(0, buf[idx])
270+
pj.write_tape(0, 'f')
277271

278272
case 'n':
279273
if !isValidNullAtom(buf[idx:]) {
280274
goto fail
281275
}
282-
pj.write_tape(0, buf[idx])
283-
284-
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
285-
if !addNumber(buf[idx:], &pj.ParsedJson) {
286-
goto fail
287-
}
276+
pj.write_tape(0, 'n')
288277

289278
case '-':
290279
if !addNumber(buf[idx:], &pj.ParsedJson) {
@@ -293,17 +282,23 @@ object_key_state:
293282

294283
case '{':
295284
pj.containingScopeOffset = append(pj.containingScopeOffset, (pj.get_current_loc()<<retAddressShift)|retAddressObjectConst)
296-
pj.write_tape(0, buf[idx])
285+
pj.write_tape(0, '{')
297286
// we have not yet encountered } so we need to come back for it
298287
goto object_begin
299288

300289
case '[':
301290
pj.containingScopeOffset = append(pj.containingScopeOffset, (pj.get_current_loc()<<retAddressShift)|retAddressObjectConst)
302-
pj.write_tape(0, buf[idx])
291+
pj.write_tape(0, '[')
303292
// we have not yet encountered } so we need to come back for it
304293
goto arrayBegin
305294

306295
default:
296+
if buf[idx] >= '0' && buf[idx] <= '9' {
297+
if !addNumber(buf[idx:], &pj.ParsedJson) {
298+
goto fail
299+
}
300+
break
301+
}
307302
goto fail
308303
}
309304

@@ -372,39 +367,45 @@ mainArraySwitch:
372367
if !isValidTrueAtom(buf[idx:]) {
373368
goto fail
374369
}
375-
pj.write_tape(0, buf[idx])
370+
pj.write_tape(0, 't')
376371

377372
case 'f':
378373
if !isValidFalseAtom(buf[idx:]) {
379374
goto fail
380375
}
381-
pj.write_tape(0, buf[idx])
376+
pj.write_tape(0, 'f')
382377

383378
case 'n':
384379
if !isValidNullAtom(buf[idx:]) {
385380
goto fail
386381
}
387-
pj.write_tape(0, buf[idx])
382+
pj.write_tape(0, 'n')
388383
/* goto array_continue */
389384

390-
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
385+
case '-':
391386
if !addNumber(buf[idx:], &pj.ParsedJson) {
392387
goto fail
393388
}
394389

395390
case '{':
396391
// we have not yet encountered ] so we need to come back for it
397392
pj.containingScopeOffset = append(pj.containingScopeOffset, (pj.get_current_loc()<<retAddressShift)|retAddressArrayConst)
398-
pj.write_tape(0, buf[idx]) // here the compilers knows what c is so this gets optimized
393+
pj.write_tape(0, '{') // here the compilers knows what c is so this gets optimized
399394
goto object_begin
400395

401396
case '[':
402397
// we have not yet encountered ] so we need to come back for it
403398
pj.containingScopeOffset = append(pj.containingScopeOffset, (pj.get_current_loc()<<retAddressShift)|retAddressArrayConst)
404-
pj.write_tape(0, buf[idx]) // here the compilers knows what c is so this gets optimized
399+
pj.write_tape(0, '[') // here the compilers knows what c is so this gets optimized
405400
goto arrayBegin
406401

407402
default:
403+
if buf[idx] >= '0' && buf[idx] <= '9' {
404+
if !addNumber(buf[idx:], &pj.ParsedJson) {
405+
goto fail
406+
}
407+
break
408+
}
408409
goto fail
409410
}
410411

0 commit comments

Comments
 (0)