1# A Journey building a fast JSON parser and full JSONPath, Oj for Go
2
3I had a dream. I'd write a fast JSON parser, generic data, and a
4JSONPath implementation and it would be beautiful, well organized, and
5something to be admired. Well, reality kicked in and laughed at those
6dreams. A Go JSON parser and tools could be high performance but to
7get that performance compromises in beauty would have to be made. This
8is a tale of journey that ended with a Parser that leaves the Go JSON
9parser in the dust and resulted in some useful tools including a
10complete and efficient JSONPath implementation.
11
12In all fairness I did embark on with some previous experience. Having
13written two JSON parser before. Both the Ruby
14[Oj](https://github.com/ohler55/oj) and the C parser
15[OjC](https://github.com/ohler55/ojc). Why not an
16[OjG](https://github.com/ohler55/ojg) for go.
17
18## Planning
19
20Like any journey it starts with the planning. Yeah, I know, it's called
21requirement gathering but casting it as planning a journey is more fun
22and this was all about enjoying the discoveries on the journey. The
23journey takes place in the land of OjG which stands for Oj for
24Go. [Oj](https://github.com/ohler55/oj) or Optimized JSON being a
25popular gem I wrote for Ruby.
26
27First, JSON parsing and any frequently used operations such as
28JSONPath evaluation had to be fast over everything else. With the
29luxury of not having to follow the existing Go json package API the
30API could be designed for the best performance.
31
32The journey would visit several areas each with its own landscape and
33different problems to solve.
34
35### Generic Data
36
37The first visit was to generic data. Not to be confused with the
38proposed Go generics. Thats a completely different animal and has
39nothing to do with whats being referred to as generic data here. In
40building tools or packages for reuse the data acted on by those tools
41needs to be navigable.
42
43Reflection can be used but that gets a bit tricky when dealing with
44private fields or field that can't be converted to something that can
45say be written as a JSON element. Other options are often better.
46
47Another approach is to use simple Go types such as `bool`, `int64`,
48`[]interface{}`, and other types that map directly on to JSON or some
49other subset of all possible Go types. If too open, such as with
50`[]interface{}` it is still possible for the user to put unsupported
51types into the data. Not to pick out any package specifically but it
52is frustrating to see an argument type of `interface{}` in an API and
53then no documentation describing that the supported types are.
54
55There is another approach though: Define a set of types that can be in
56a collection and use those types. With this approach, the generic data
57implementation has to support the basic JSON types of `null`,
58`boolean`, `int64`, `float64`, `string`, array, and object. In
59addition time should be supported. From experience in both JSON use in
60Ruby and Go time has always been needed. Time is just too much a part
61of any set of data to leave it out.
62
63The generic data had to be type safe. It would not do to have an
64element that could not be encoded as JSON in the data.
65
66A frequent operation for generic data is to store that data into a
67JSON database or similar. That meant converting to simple Go types of
68`nil`, `bool`, `int64`, `float64`, `string`, `[]interface{}`, and
69`map[string]interface{}` had to be fast.
70
71Also planned for this part of the journey was methods on the types to
72support getting, setting, and deleting elements using JSONPath. The
73hope was to have an object based approach to the generic nodes so
74something like the following could be used but keeping generic data,
75JSONPath, and parsing in separate packages.
76
77```golang
78    var n gen.Node
79    n = gen.Int(123)
80    i, ok := n.AsInt()
81```
82
83Unfortunately that part of the journey had to be cancelled as the Go
84travel guide refuses to let packages talk back and forth. Imports are
85one way only. After trying to put all the code in one package it
86eventually got unwieldy. Function names started being prefixed with
87what should really have been package names so the object and method
88approach was dropped. A change in API but the journey would continue.
89
90### JSON Parser and Validator
91
92The next stop was the parser and validator. After some consideration
93it seemed like starting with the validator would be best way to become
94familiar with the territory. The JSON parser and validator need not be
95the same and each should be as performant as possible. The parsers
96needed to support parsing into simple Go types as well as the generic
97data types.
98
99When parsing files that include millions or more JSON elements in
100files that might be over 100GB a streaming parser is necessary. It
101would be nice to share some code with both the streaming and string
102parsers of course. It's easier to pack light when the areas are
103similar.
104
105The parser must also allow parsing into native Go types. Furthermore
106interfaces must be supported even though Go unmarshalling does not
107support interface fields. Many data types make use of interfaces
108that limitation was not acceptable for the OjG parser. A different
109approach to support interfaces was possible.
110
111JSON documents of any non-trivial size, especially if hand-edited, are
112likely to have errors at some point. Parse errors must identify where
113in the document the error occurred.
114
115### JSONPath
116
117Saving the most interesting part of the trip for last, the JSONPath
118implementation promised to have all sorts of interesting problems to
119solve with descents, wildcards, and especially filters.
120
121A JSONPath is used to extract elements from data. That part of the
122implementation had to be fast. Parsing really didn't have to be fast
123but it would be nice to have a way of building a JSONPath in a
124performant manner even if it was not as convenient as parsing a
125string.
126
127The JSONPath implementation had to implement all the features
128described by the [Goessner
129article](https://goessner.net/articles/JsonPath). There are other
130descriptions of JSONPath but the Goessner description is the most
131referenced. Since the implementation is in Go the scripting feature
132described could be left out as long as similar functionality could be
133provided for array indexes relative to the length of the
134array. Borrowing from Ruby, using negative indexes would provide that
135functionality.
136
137## The Journey
138
139The journey unfolded as planned to a degree. There were some false
140starts and revisits but eventually each destination was reached and
141the journey completed.
142
143### Generic Data (`gen` package)
144
145What better way to make generic type fast than to just define generic
146types from simple Go types and then add methods on those types? A
147`gen.Int` is just an `int64` and a `gen.Array` is just a
148`[]gen.Node`. With that approach there are no extra allocations.
149
150```golang
151type Node interface{}
152type Int int64
153type Array []Node
154```
155
156Since generic arrays and objects restrict the type of the values in
157each collection to `gen.Node` types the collections are assured to
158contain only elements that can be encoded as JSON.
159
160Methods on the `Node` could not be implemented without import loops so
161the number of functions in the `Node` interface were limited. It was
162clear a parser specific to the generic data type would be needed but
163that would have to wait until the parser part of the journey was
164completed. Then the generic data package could be revisited and the
165parser explored.
166
167Peeking at the future to the generic data parser revisit it was not
168very interesting after the deep dive into the simple data parser. The
169parser for generic types is a copy of the oj package parser but
170instead of simple types being created instances that support the
171`gen.Node` interface are created.
172
173### Simple Parser (`oj` package)
174
175Looking back its hard to say what was the most interesting part of the
176journey, the parser or JSONPath. Each had their own unique set of
177issues. The parser was the best place to start though as some valuable
178lessons were learned about what to avoid and what to gravitate toward
179in trying to achieve high performance Go code.
180
181#### Validator
182
183From the start I knew that a single pass parser would be more
184efficient than building tokens and then making a second pass to decide
185what the tokens means. At least that approach as worked well in the
186past. I dived in and used a `readValue` function that branched
187depending on the next character read. It worked but it was slower than
188the target of being on par with the Go `json.Validate`. That was the
189bar to clear. The first attempt was off by a lot. Of course a
190benchmark was needed to verify that so the `cmd/benchmark` command was
191started. Profiling didn't help much. It turned out since much of the
192overhead was in the function call setup which isn't obvious when
193profiling.
194
195Not knowing at the time that function calls were so expensive but
196anticipating that there was some overhead in function calls I moved
197some of the code from a few frequently called functions to be inline
198in the calling function. That made much more of a difference than I
199expected. At that point I looked at the Go code for the core
200validation code. I was surprised to see that it used lots of functions
201but not functions attached to a type. I gave that approach a try but
202with functions on the parser type. The results were not good
203either. Simply changing the functions to take the parser as an
204argument made a big difference though. Another lesson learned.
205
206Next was to remove function calls as much as possible since they did
207seem to be expensive. The code was no longer elegant and had lots of
208duplicated blocks but it ran much faster. At this point the code
209performance was getting closer to clearing the Go validator bar.
210
211When parsing in a single pass a conceptual state machine is generally
212used. When branching with functions there is still a state machine but
213the states are limited for each function making it much easier to
214follow. Moving into a single function meant tracking many more states
215in single state machine. Implementation was with lengthy switch
216statements. One problem remained though. Array and Object had to be
217tracked to know when a closing `]` or `}` was allowed. Since function
218calls were being avoided that meant maintaining a stack in the single
219parser function. That approach worked well with very little overhead.
220
221Another tweak was to reuse memory. At this point the parser only
222allocated a few objects but why would it need to allocate any if the
223buffers for the stack could be reused. That prompted a change in the
224API. The initial API was for a single `Validate()` function. If the
225validator was made public it could be reused. That made a lot of sense
226since often similar data is parsed or validated by the same
227application. That change was enough to reduce the allocations per
228validation to zero and brought the performance under the Go
229`json.Valid()` bar.
230
231#### Parser
232
233With the many optimum techniques identified while visiting the
234validator, the next part of the journey was to use those same
235technique on the parser.
236
237The difference between the validator and the parser is that the parser
238needs to build up data elements. The first attempt was to add the
239bytes associated with a value to a reusable buffer and then parse that
240buffer at the end of the value bytes in the source. It worked and was
241as fast as the `json.Unmarshall` function but that was not enough as
242there were still more allocations than seemed necessary.
243
244By expanding the state machine `null`, `true`, and `false` could be
245identified as values without adding to the buffer. That gave a
246bit of improvement.
247
248Numbers, specifically integers, were another value type that really
249didn't need to be parsed from a buffer so instead of appending bytes
250to a buffer and calling `strconv.ParseInt()`, integer values were
251built as an `int64` and grown as bytes were read. If a `.` character
252is encountered then the number is a decimal so the type expected is
253changed and each part of a float is captured as integers and finally a
254float64 is created when done. This was another improvement in
255performance.
256
257Not much could be done to improve string parsing since it is really
258just appending bytes to a buffer and making them a string at the final
259`"`. Each byte being appended needed to be checked though. A byte map
260in the form of a 256 long bytes array is used for that purpose.
261
262Going back to the stack used in the validator, instead of putting a
263simple marker on the stack like the validator, when an Object start
264character, a `{` is encountered a new `map[string]interface{}` is put
265on the stack. Values and keys are then used to set members of the
266map. Nothing special there.
267
268Saving the best for last, arrays were tougher to deal with. A value is
269not just added to an array but rather appended to an array and a
270potentially new array is returned. Thats not a terribly efficient way to
271build a slice as it will go through multiple reallocations. Instead, a
272second slice index stack is kept. As an array is to be created, a spot
273is reserved on the stack and the index of that stack location is
274placed on the slice index stack. After that values are pushed onto the
275stack until an array close character `]` is reached. The slice index
276is then referenced and a new `[]interface{}` is allocated for all the
277values from the arry index on the stack to the end of the
278stack. Values are copied to the new array and the stack is collapsed
279to the index. A bit complicated but it does save multiple object
280allocations.
281
282After some experimentation it turned out that the overhead of some
283operations such as creating a slice or adding a number were not
284impacted to any large extent by making a function call since it does
285not happen as frequently as processing each byte. Some use of
286functions could therefor be used to remove duplicate code without
287incurring a significant performance impact.
288
289One stop left at the parser package tour. Streaming had to be
290supported. At this point there were already plans on how to deal with
291streaming which was to load up a buffer and iterate over that buffer
292using the exact same code as for parsing bytes and repeat until there
293was nothing left to read. It seemed like using an index into the
294buffer would be easier to keep track of but switching from a `for`
295`range` to `for i = 0; i < size; i++ {` dropped the performance
296considerably. Clearly staying with the `range` approach was
297better. Once that was working a quick trip back to the validator to
298allow it to support streams was made.
299
300Stream parsing or parsing a string with multiple JSON documents in it
301is best handled with a callback function. That allows the caller to
302process the parsed document and move on without incurring any
303additional memory allocations unless needed.
304
305The stay at the validator and parser was fairly lengthy at a bit over
306a month of evening coding.
307
308### JSONPath (`jp` package)
309
310The visit to JSONPath would prove to be a long stay as well with a lot
311more creativity for some tantalizing problems.
312
313The first step was to get a language and cultural refresher on
314JSONPath terms and behavior. From that it was decided that a JSONPath
315would be represented by a `jp.Expr` which is composed of fragments or
316`jp.Frag` objects. Keeping with the guideline of minimizing
317allocations the `jp.Expr` is just a slice of `jp.Frag`. In most cases
318expressions are defined statically so the parser need not be fast. No
319special care was taken to make the JSONPath parser fast. Instead
320functions are used in an approach that is easier to understand. I said
321easier, not easy. There are a fair number of dangerous curves with
322trying to support bracketed notation as well as dot notation and how
323that all plays nicely with the script parser so that one can call the
324other to support nested filters. It was rewarding to see it all come
325together though.
326
327If the need exists to create expressions at run time then functions
328are used that allow them to be constructed more easily. That makes for
329a lot of functions. I also like to be able to keep code compact and
330figured others might too so each fragment type can also be created
331with a single letter function. They don't have to be used but they
332exist to support building expressions as a chain.
333
334```golang
335    x := jp.R().D().C("abc").W().C("xyz").N(3)
336    fmt.Println(x.String())
337    // $..abc.*.xyz[3]
338```
339
340contrasted with the use of the JSONPath parser:
341
342```golang
343    x, err := jp.ParseString("$..abc.*.xyz[3]")
344    // check err first
345    fmt.Println(x.String())
346    // $..abc.*.xyz[3]
347```
348
349Evaluating an expression against data involves walking down the data
350tree to find one or more elements. Conceptually each fragment of a
351path sets up zero or more paths to follow through the data. When the
352last fragment is reached the search is done. A recursive approach
353would be ideal where the evaluation of one fragment then invokes the
354next fragment's eval function with as many paths it matches. Great on
355paper but for something like a descent fragment (`..`) that is a lot
356of function calls.
357
358Given that function calls are expensive and slices are cheap a Forth
359(the language) evaluation stack approach is used. Not exactly Forth
360but a similar concept mixing data and operators. Each fragment takes
361its matches and those matches already on the stack. Then the next
362fragment evaluates each in turn. This continues until the stack
363shrinks back to one element indicating the evaluation is complete. The
364last fragment puts any matches on a results list which is returned
365instead of on the stack.
366
367 | Stack  | Frag  |
368 | ------ | ----- |
369 | {a:3}  | data  |
370 | 'a'    | Child |
371
372One fragment type is a filter which looks like `[?(@.x == 3)]`. This
373requires a script or function evaluation. A similar stack based
374approach is used for evaluating scripts. Note that scripts can and
375almost always contain a JSONPath expression starting with a `@`
376character. An interesting aspect of this is that a filter can contain
377other filters. OjG supports nested filters.
378
379The most memorable part of the JSONPath part of the journey had to be
380the evaluation stack. That worked out great and was able to support
381all the various fragment types.
382
383### Converting or Altering Data (`alt` package)
384
385A little extra was added to the journey once it was clear the generic
386data types would not support JSONPath directly. The original plan was
387to has functions like `AsInt()` as part of the `Node` interface. With
388that no longer reasonable an `alt` package became part of the
389journey. It would be used for converting types as well as altering
390existing ones. To make the last part of the trip even more interesting
391the `alt` package is where marshalling and unmarshalling types came
392into play but under the names of recompose and decompose since
393operations were to take Go types and decompose those objects into
394simple or generic data. The reverse is to recompose the simple data
395back into their original types. This takes an approach used in Oj for
396Ruby when the type name is encoded in the decomposed data. Since the
397data type is included in the data itself it is self describing and can
398be used to recompose types that include interface members.
399
400There is a trade off in that JSON is not parsed directly to a Go type
401by must go through an intermediate data structure first. There is an
402up side to that as well though. Now any simple or generic data can be
403used to recompose objects and not just JSON strings.
404
405The `alt.GenAlter()` function was interesting in that it is possible
406to modify a slice type and then reset the members without
407reallocating.
408
409Thats the last stop of the journey.
410
411## Lessons Learned
412
413Benchmarking was instrumental to tuning and picking the most favorable
414approach to the implementation. Through those benchmarks a number of
415lessons were learned.  The final benchmarks results can be viewed by
416running the `cmd/benchmark` command. See the results at
417[benchmarks.md](benchmarks.md).
418
419Here is a snippet from the benchmarks. Note higher is better for the
420numbers in parenthesis which is a ratio of the OjG component to Go
421json package component.
422
423```
424Parse JSON
425json.Unmarshal:           7104 ns/op (1.00x)    4808 B/op (1.00x)      90 allocs/op (1.00x)
426  oj.Parse:               4518 ns/op (1.57x)    3984 B/op (1.21x)      86 allocs/op (1.05x)
427  oj.GenParse:            4623 ns/op (1.54x)    3984 B/op (1.21x)      86 allocs/op (1.05x)
428
429json.Marshal:             2616 ns/op (1.00x)     992 B/op (1.00x)      22 allocs/op (1.00x)
430  oj.JSON:                 436 ns/op (6.00x)     131 B/op (7.57x)       4 allocs/op (5.50x)
431  oj.Write:                455 ns/op (5.75x)     131 B/op (7.57x)       4 allocs/op (5.50x)
432```
433
434### Functions Add Overhead
435
436Sure we all know a function call add some overhead in any language. In
437C that overhead is pretty small or nonexistent with inline
438functions. That is not true for Go. There is considerable overhead in
439making a function call and if that functional call included any kind
440of context such as being the function of a type the overhead is even
441higher. That observation (while disappointing) drove a lot of the
442parser and JSONPath evaluation code. For nice looking and well
443organized code using functions are highly recommended but for high
444perfomance find a way to reduce function calls.
445
446The implementation of the parser included a lot of duplicate code to
447reduce function calls and it did make a significant difference in
448performance.
449
450The JSONPath evaluation takes an additional approach. It includes a
451fair amount of code duplication but it also implements its own stack
452to avoid nested functional calls even though the nature of the
453evaluation is a better match for a recursive implementation.
454
455### Slices are Nice
456
457Slices are implemented very efficiently in Go. Appending to a slice
458has very little overhead. Reusing slices by collapsing them to zero
459length is a great way to avoid allocating additional memory. Care has
460to be taken when collapsing though as any cells in the slice that
461point to objects will now leave those objects dangling or rather
462referenced but not reachable and they will never be garbage
463collected. Simply setting the slice slot to `nil` will avoid memory
464leaks.
465
466### Memory Allocation
467
468Like most languages, memory allocation adds overhead. It's best to avoid
469when possible. A good example of that is in the `alt` package. The
470`Alter()` function replaces slice and map members instead of
471allocating a new slice or map when possible.
472
473Parsers take advantage by reusing buffers and avoiding allocation of
474token during possible when possible.
475
476### Range Has Been Optimized
477
478Using a `for` `range` loop is better than incrementing an index. The
479difference was not huge but was noticable.
480
481### APIs Matter
482
483It's important to define an API that is easy to use as well as one
484that allows for the best performance. The parser as well as the
485JSONPath builders attempt to do both. An even better example is the
486[GGql](https://github.com/uhn/ggql) GraphQL package. It provides a
487very simple API when compared to previous Go GraphQL packages and it
488is many times
489[faster](https://github.com/the-benchmarker/graphql-benchmarks).
490
491## Whats Next?
492
493Theres alway something new ready to be explored. For OjG there are a few things in the planning stage.
494
495 - A short trip to Regex filters for JSONPath.
496 - A construction project to add JSON building to the **oj** command which is an alternative to jq but using JSONPath.
497 - Explore new territory by implementing a Simple Encoding Notation which mixes GraphQL syntax with JSON for simpler more forgiving format.
498 - A callback parser along the lines of the Go json.Decoder or more likely like the Oj [Simple Callback Parser](http://ohler.com/oj/doc/Oj.html#sc_parse-class_method).
499
500Discuss this on [Changelog News](https://changelog.com/news/a-journey-building-a-fast-json-parser-and-full-jsonpath-oj-for-go-YRXJ).
501