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