From 0e6e567424abc487ae0256a87ba4859b1c2ecb6f Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Fri, 19 Aug 2016 11:22:59 -0700 Subject: [PATCH] optimistic optimizations --- README.md | 16 +++--- gjson.go | 137 +++++++++++++++++++++++++++----------------------- gjson_test.go | 75 +++++++++------------------ 3 files changed, 104 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 634713c..cba0896 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,13 @@ Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/js and [jsonparser](https://github.com/buger/jsonparser) ``` -BenchmarkGJSONGet-8 3000000 440 ns/op 0 B/op 0 allocs/op -BenchmarkJSONUnmarshalMap-8 600000 10738 ns/op 3176 B/op 69 allocs/op -BenchmarkJSONUnmarshalStruct-8 600000 11635 ns/op 1960 B/op 69 allocs/op -BenchmarkJSONDecoder-8 300000 17193 ns/op 4864 B/op 184 allocs/op -BenchmarkFFJSONLexer-8 1500000 3773 ns/op 1024 B/op 8 allocs/op -BenchmarkEasyJSONLexer-8 3000000 1134 ns/op 741 B/op 6 allocs/op -BenchmarkJSONParserGet-8 3000000 596 ns/op 21 B/op 0 allocs/op +BenchmarkGJSONGet-8 3000000 373 ns/op 0 B/op 0 allocs/op +BenchmarkJSONUnmarshalMap-8 600000 8884 ns/op 3048 B/op 69 allocs/op +BenchmarkJSONUnmarshalStruct-8 600000 9045 ns/op 1832 B/op 69 allocs/op +BenchmarkJSONDecoder-8 300000 14134 ns/op 4224 B/op 184 allocs/op +BenchmarkFFJSONLexer-8 1500000 3182 ns/op 896 B/op 8 allocs/op +BenchmarkEasyJSONLexer-8 3000000 932 ns/op 613 B/op 6 allocs/op +BenchmarkJSONParserGet-8 3000000 444 ns/op 21 B/op 0 allocs/op ``` JSON document used: @@ -177,7 +177,7 @@ widget.text.onMouseUp ``` -*These are the results from running the benchmarks on a MacBook Pro 15" 2.8 GHz Intel Core i7:* +*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7.* ## Contact Josh Baker [@tidwall](http://twitter.com/tidwall) diff --git a/gjson.go b/gjson.go index f251d8b..7443189 100644 --- a/gjson.go +++ b/gjson.go @@ -133,44 +133,48 @@ func Get(json string, path string) Result { // parse the path. just split on the dot for i := 0; i < len(path); i++ { next_part: - if path[i] == '\\' { - // go into escape mode - epart := []byte(path[s:i]) - i++ - if i < len(path) { - epart = append(epart, path[i]) + // be optimistic that the path mostly contains lowercase and + // underscore characters. + if path[i] <= '\\' { + if path[i] == '\\' { + // go into escape mode. + epart := []byte(path[s:i]) i++ - for ; i < len(path); i++ { - if path[i] == '\\' { - i++ - if i < len(path) { - epart = append(epart, path[i]) - } - continue - } else if path[i] == '.' { - parts = append(parts, part{wild: wild, key: string(epart)}) - if wild { - wild = false - } - s = i + 1 - i++ - goto next_part - } else if path[i] == '*' || path[i] == '?' { - wild = true - } + if i < len(path) { epart = append(epart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + if i < len(path) { + epart = append(epart, path[i]) + } + continue + } else if path[i] == '.' { + parts = append(parts, part{wild: wild, key: string(epart)}) + if wild { + wild = false + } + s = i + 1 + i++ + goto next_part + } else if path[i] == '*' || path[i] == '?' { + wild = true + } + epart = append(epart, path[i]) + } } + parts = append(parts, part{wild: wild, key: string(epart)}) + goto end_parts + } else if path[i] == '.' { + parts = append(parts, part{wild: wild, key: path[s:i]}) + if wild { + wild = false + } + s = i + 1 + } else if path[i] == '*' || path[i] == '?' { + wild = true } - parts = append(parts, part{wild: wild, key: string(epart)}) - goto end_parts - } else if path[i] == '.' { - parts = append(parts, part{wild: wild, key: path[s:i]}) - if wild { - wild = false - } - s = i + 1 - } else if path[i] == '*' || path[i] == '?' { - wild = true } } parts = append(parts, part{wild: wild, key: path[s:]}) @@ -187,33 +191,37 @@ end_parts: // look for first delimiter for ; i < len(json); i++ { - if json[i] > ' ' { - if json[i] == '{' { - f.stype = '{' - } else if json[i] == '[' { - f.stype = '[' - } else { - // not a valid type - return Result{} - } + if json[i] == '{' { + f.stype = '{' + i++ + stack[0].stype = f.stype + break + } else if json[i] == '[' { + f.stype = '[' + stack[0].stype = f.stype i++ break + } else if json[i] <= ' ' { + continue + } else { + return Result{} } } - stack[0].stype = f.stype - - // search for key + // read the next key read_key: if f.stype == '[' { + // for arrays we use the index of the value as the key. + // so "0" is the key for the first value, and "10" is the + // key for the 10th value. f.key = strconv.FormatInt(int64(f.count), 10) f.count++ } else { + // for objects we must parse the next string. for ; i < len(json); i++ { + // read string if json[i] == '"' { - //read to end of key i++ - // readstr // the first double-quote has already been read s = i for ; i < len(json); i++ { @@ -249,16 +257,17 @@ read_key: } break } + // end read string } } - // end readstr - // we have a brand new key. + // we have a brand new (possibly shiny) key. // is it the key that we are looking for? if parts[depth-1].wild { // it's a wildcard path element matched = wildcardMatch(f.key, parts[depth-1].key) } else { + // just a straight up equality check matched = parts[depth-1].key == f.key } @@ -325,22 +334,16 @@ read_key: proc_delim: if (matched && depth == len(parts)) || !matched { - // -- BEGIN SQUASH -- // + // begin squash // squash the value, ignoring all nested arrays and objects. s = i - 1 // the first '[' or '{' has already been read depth := 1 + squash: for ; i < len(json); i++ { if json[i] >= '"' && json[i] <= '}' { - if json[i] == '{' || json[i] == '[' { - depth++ - } else if json[i] == '}' || json[i] == ']' { - depth-- - if depth == 0 { - i++ - break - } - } else if json[i] == '"' { + switch json[i] { + case '"': i++ s2 := i for ; i < len(json); i++ { @@ -361,14 +364,19 @@ proc_delim: break } } - if i == len(json) { - break + case '{', '[': + depth++ + case '}', ']': + depth-- + if depth == 0 { + i++ + break squash } } } } - value.Raw = json[s:i] - // -- END SQUASH -- // + // end squash + // the 'i' and 's' values should fall-though to the proc_val function } // process the value @@ -379,6 +387,7 @@ proc_val: switch vc { case '{', '[': value.Type = JSON + value.Raw = json[s:i] case 'n': value.Type = Null case 't': diff --git a/gjson_test.go b/gjson_test.go index dc704bd..2576399 100644 --- a/gjson_test.go +++ b/gjson_test.go @@ -222,60 +222,31 @@ func TestLess(t *testing.T) { assert(t, stringLessInsensitive("124abcde", "125abcde")) } -/* -func TestTwitter(t *testing.T) { - data, err := ioutil.ReadFile("twitter.json") - if err != nil { - return - } - token := Get(string(data), "search_metadata.max_id") - if token.Num != 505874924095815700 { - t.Fatalf("expecting %d\n", 505874924095815700) - } - -} -func BenchmarkTwitter(t *testing.B) { - // the twitter.json file must be present - data, err := ioutil.ReadFile("twitter.json") - if err != nil { - return - } - json := string(data) - t.ResetTimer() - for i := 0; i < t.N; i++ { - token := Get(json, "search_metadata.max_id") - if token.Type != Number || token.Raw != "505874924095815700" || token.Num != 505874924095815700 { - t.Fatal("invalid response") +var exampleJSON = `{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" } } -} -*/ - -var exampleJSON = ` -{"widget": { - "debug": "on", - "window": { - "title": "Sample Konfabulator Widget", - "name": "main_window", - "width": 500, - "height": 500 - }, - "image": { - "src": "Images/Sun.png", - "hOffset": 250, - "vOffset": 250, - "alignment": "center" - }, - "text": { - "data": "Click Here", - "size": 36, - "style": "bold", - "vOffset": 100, - "alignment": "center", - "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" - } -}} -` +}` type BenchStruct struct { Widget struct {