internal/encoding/json: improve decoding of JSON numbers for floats
Per Joe's suggestion, remove producing numberParts when parsing a JSON
number to produce corresponding Value. This saves having to store it
inside Value as well. Only produce numberParts for calls to
Value.{Int,Uint} call.
numberParts is only used for producing integers and removing the logic to
produce numberParts improves overall decoding speed for floats, and shows no
change for integers.
name old time/op new time/op delta
Float-4 559ns ± 0% 288ns ± 0% ~ (p=1.000 n=1+1)
Int-4 471ns ± 0% 466ns ± 0% ~ (p=1.000 n=1+1)
Change-Id: I21bf304ca67dda8d41a4ea0022dcbefd51058c1c
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/168781
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/internal/encoding/json/number.go b/internal/encoding/json/number.go
index 6b3ed86..75f6a3a 100644
--- a/internal/encoding/json/number.go
+++ b/internal/encoding/json/number.go
@@ -41,70 +41,50 @@
return out
}
-// numberParts is the result of parsing out a valid JSON number. It contains
-// the parts of a number. The parts are used for integer conversion.
-type numberParts struct {
- neg bool
- intp []byte
- frac []byte
- exp []byte
-}
-
-// parseNumber returns a numberParts instance if it is able to read a JSON
-// number from the given []byte. It also returns the number of bytes read.
-// Parsing logic follows the definition in
+// consumeNumber reads the given []byte for a valid JSON number. If it is valid,
+// it returns the number of bytes. Parsing logic follows the definition in
// https://tools.ietf.org/html/rfc7159#section-6, and is based off
// encoding/json.isValidNumber function.
-func parseNumber(input []byte) (*numberParts, int) {
+func consumeNumber(input []byte) (int, bool) {
var n int
- var neg bool
- var intp []byte
- var frac []byte
- var exp []byte
s := input
if len(s) == 0 {
- return nil, 0
+ return 0, false
}
// Optional -
if s[0] == '-' {
- neg = true
s = s[1:]
n++
if len(s) == 0 {
- return nil, 0
+ return 0, false
}
}
// Digits
switch {
case s[0] == '0':
- // Skip first 0 and no need to store.
s = s[1:]
n++
case '1' <= s[0] && s[0] <= '9':
- intp = append(intp, s[0])
s = s[1:]
n++
for len(s) > 0 && '0' <= s[0] && s[0] <= '9' {
- intp = append(intp, s[0])
s = s[1:]
n++
}
default:
- return nil, 0
+ return 0, false
}
// . followed by 1 or more digits.
if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' {
- frac = append(frac, s[1])
s = s[2:]
n += 2
for len(s) > 0 && '0' <= s[0] && s[0] <= '9' {
- frac = append(frac, s[0])
s = s[1:]
n++
}
@@ -116,15 +96,13 @@
s = s[1:]
n++
if s[0] == '+' || s[0] == '-' {
- exp = append(exp, s[0])
s = s[1:]
n++
if len(s) == 0 {
- return nil, 0
+ return 0, false
}
}
for len(s) > 0 && '0' <= s[0] && s[0] <= '9' {
- exp = append(exp, s[0])
s = s[1:]
n++
}
@@ -132,21 +110,101 @@
// Check that next byte is a delimiter or it is at the end.
if n < len(input) && isNotDelim(input[n]) {
- return nil, 0
+ return 0, false
}
- return &numberParts{
+ return n, true
+}
+
+// numberParts is the result of parsing out a valid JSON number. It contains
+// the parts of a number. The parts are used for integer conversion.
+type numberParts struct {
+ neg bool
+ intp []byte
+ frac []byte
+ exp []byte
+}
+
+// parseNumber constructs numberParts from given []byte. The logic here is
+// similar to consumeNumber above with the difference of having to construct
+// numberParts.
+func parseNumber(input []byte) (numberParts, bool) {
+ var neg bool
+ var intp []byte
+ var frac []byte
+ var exp []byte
+
+ s := input
+ if len(s) == 0 {
+ return numberParts{}, false
+ }
+
+ // Optional -
+ if s[0] == '-' {
+ neg = true
+ s = s[1:]
+ if len(s) == 0 {
+ return numberParts{}, false
+ }
+ }
+
+ // Digits
+ switch {
+ case s[0] == '0':
+ // Skip first 0 and no need to store.
+ s = s[1:]
+
+ case '1' <= s[0] && s[0] <= '9':
+ intp = append(intp, s[0])
+ s = s[1:]
+ for len(s) > 0 && '0' <= s[0] && s[0] <= '9' {
+ intp = append(intp, s[0])
+ s = s[1:]
+ }
+
+ default:
+ return numberParts{}, false
+ }
+
+ // . followed by 1 or more digits.
+ if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' {
+ frac = append(frac, s[1])
+ s = s[2:]
+ for len(s) > 0 && '0' <= s[0] && s[0] <= '9' {
+ frac = append(frac, s[0])
+ s = s[1:]
+ }
+ }
+
+ // e or E followed by an optional - or + and
+ // 1 or more digits.
+ if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') {
+ s = s[1:]
+ if s[0] == '+' || s[0] == '-' {
+ exp = append(exp, s[0])
+ s = s[1:]
+ if len(s) == 0 {
+ return numberParts{}, false
+ }
+ }
+ for len(s) > 0 && '0' <= s[0] && s[0] <= '9' {
+ exp = append(exp, s[0])
+ s = s[1:]
+ }
+ }
+
+ return numberParts{
neg: neg,
intp: intp,
frac: bytes.TrimRight(frac, "0"), // Remove unnecessary 0s to the right.
exp: exp,
- }, n
+ }, true
}
// normalizeToIntString returns an integer string in normal form without the
// E-notation for given numberParts. It will return false if it is not an
// integer or if the exponent exceeds than max/min int value.
-func normalizeToIntString(n *numberParts) (string, bool) {
+func normalizeToIntString(n numberParts) (string, bool) {
num := n.intp
intpSize := len(num)
fracSize := len(n.frac)