github.com/3JoB/go-json@v0.10.4/README.md (about)

     1  # go-json
     2  
     3  ![Go](https://github.com/3JoB/go-json/workflows/Go/badge.svg)
     4  [![GoDoc](https://godoc.org/github.com/3JoB/go-json?status.svg)](https://pkg.go.dev/github.com/3JoB/go-json?tab=doc)
     5  [![codecov](https://codecov.io/gh/goccy/go-json/branch/master/graph/badge.svg)](https://codecov.io/gh/goccy/go-json)
     6  
     7  Fast JSON encoder/decoder compatible with encoding/json for Go
     8  
     9  <img width="400px" src="https://user-images.githubusercontent.com/209884/92572337-42b42900-f2bf-11ea-973a-c74a359553a5.png"></img>
    10  
    11  # Roadmap
    12  
    13  ```
    14  * version ( expected release date )
    15  
    16  * v0.9.0
    17   |
    18   | while maintaining compatibility with encoding/json, we will add convenient APIs
    19   |
    20   v
    21  * v1.0.0
    22  ```
    23  
    24  We are accepting requests for features that will be implemented between v0.9.0 and v.1.0.0.
    25  If you have the API you need, please submit your issue [here](https://github.com/3JoB/go-json/issues).
    26  
    27  # Features
    28  
    29  - Drop-in replacement of `encoding/json`
    30  - Fast ( See [Benchmark section](https://github.com/3JoB/go-json#benchmarks) )
    31  - Flexible customization with options
    32  - Coloring the encoded string
    33  - Can propagate context.Context to `MarshalJSON` or `UnmarshalJSON`
    34  - Can dynamically filter the fields of the structure type-safely
    35  
    36  # Installation
    37  
    38  ```
    39  go get github.com/3JoB/go-json
    40  ```
    41  
    42  # How to use
    43  
    44  Replace import statement from `encoding/json` to `github.com/3JoB/go-json`
    45  
    46  ```
    47  -import "encoding/json"
    48  +import "github.com/3JoB/go-json"
    49  ```
    50  
    51  # JSON library comparison
    52  
    53  |  name  |  encoder | decoder | compatible with `encoding/json` |
    54  | :----: | :------: | :-----: | :-----------------------------: |
    55  | encoding/json |  yes | yes | N/A |
    56  | [json-iterator/go](https://github.com/json-iterator/go) | yes | yes | partial |
    57  | [easyjson](https://github.com/mailru/easyjson) | yes | yes |  no |
    58  | [gojay](https://github.com/francoispqt/gojay) | yes | yes |  no |
    59  | [segmentio/encoding/json](https://github.com/segmentio/encoding/tree/master/json) | yes | yes | partial |
    60  | [jettison](https://github.com/wI2L/jettison) | yes | no | no |
    61  | [simdjson-go](https://github.com/minio/simdjson-go) | no | yes | no |
    62  | goccy/go-json | yes | yes | yes |
    63  
    64  - `json-iterator/go` isn't compatible with `encoding/json` in many ways (e.g. https://github.com/json-iterator/go/issues/229 ), but it hasn't been supported for a long time.
    65  - `segmentio/encoding/json` is well supported for encoders, but some are not supported for decoder APIs such as `Token` ( streaming decode )
    66  
    67  ## Other libraries
    68  
    69  - [jingo](https://github.com/bet365/jingo)
    70  
    71  I tried the benchmark but it didn't work.
    72  Also, it seems to panic when it receives an unexpected value because there is no error handling...
    73  
    74  - [ffjson](https://github.com/pquerna/ffjson)
    75  
    76  Benchmarking gave very slow results.
    77  It seems that it is assumed that the user will use the buffer pool properly.
    78  Also, development seems to have already stopped
    79  
    80  # Benchmarks
    81  
    82  ```
    83  $ cd benchmarks
    84  $ go test -bench .
    85  ```
    86  
    87  ## Encode
    88  
    89  <img width="700px" src="https://user-images.githubusercontent.com/209884/107126758-0845cb00-68f5-11eb-8db7-086fcf9bcfaa.png"></img>
    90  <img width="700px" src="https://user-images.githubusercontent.com/209884/107126757-07ad3480-68f5-11eb-87aa-858cc5eacfcb.png"></img>
    91  
    92  ## Decode
    93  
    94  <img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979944-bd1d6d80-7002-11eb-944b-9d17b6674e3f.png">
    95  <img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979931-b989e680-7002-11eb-87a0-66fc22d90dd4.png">
    96  <img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979940-bc84d700-7002-11eb-9647-869bbc25c9d9.png">
    97  
    98  
    99  # Fuzzing
   100  
   101  [go-json-fuzz](https://github.com/3JoB/go-json-fuzz) is the repository for fuzzing tests.
   102  If you run the test in this repository and find a bug, please commit to corpus to go-json-fuzz and report the issue to [go-json](https://github.com/3JoB/go-json/issues).
   103  
   104  # How it works
   105  
   106  `go-json` is very fast in both encoding and decoding compared to other libraries.
   107  It's easier to implement by using automatic code generation for performance or by using a dedicated interface, but `go-json` dares to stick to compatibility with `encoding/json` and is the simple interface. Despite this, we are developing with the aim of being the fastest library.
   108  
   109  Here, we explain the various speed-up techniques implemented by `go-json`.
   110  
   111  ## Basic technique
   112  
   113  The techniques listed here are the ones used by most of the libraries listed above.
   114  
   115  ### Buffer reuse
   116  
   117  Since the only value required for the result of `json.Marshal(interface{}) ([]byte, error)` is `[]byte`, the only value that must be allocated during encoding is the return value `[]byte` .
   118  
   119  Also, as the number of allocations increases, the performance will be affected, so the number of allocations should be kept as low as possible when creating `[]byte`.
   120  
   121  Therefore, there is a technique to reduce the number of times a new buffer must be allocated by reusing the buffer used for the previous encoding by using `sync.Pool`.
   122  
   123  Finally, you allocate a buffer that is as long as the resulting buffer and copy the contents into it, you only need to allocate the buffer once in theory.
   124  
   125  ```go
   126  type buffer struct {
   127      data []byte
   128  }
   129  
   130  var bufPool = sync.Pool{
   131      New: func() interface{} {
   132          return &buffer{data: make([]byte, 0, 1024)}
   133      },
   134  }
   135  
   136  buf := bufPool.Get().(*buffer)
   137  data := encode(buf.data) // reuse buf.data
   138  
   139  newBuf := make([]byte, len(data))
   140  copy(newBuf, buf)
   141  
   142  buf.data = data
   143  bufPool.Put(buf)
   144  ```
   145  
   146  ### Elimination of reflection
   147  
   148  As you know, the reflection operation is very slow.
   149  
   150  Therefore, using the fact that the address position where the type information is stored is fixed for each binary ( we call this `typeptr` ),
   151  we can use the address in the type information to call a pre-built optimized process.
   152  
   153  For example, you can get the address to the type information from `interface{}` as follows and you can use that information to call a process that does not have reflection.
   154  
   155  To process without reflection, pass a pointer (`unsafe.Pointer`) to the value is stored.
   156  
   157  ```go
   158  
   159  type emptyInterface struct {
   160      typ unsafe.Pointer
   161      ptr unsafe.Pointer
   162  }
   163  
   164  var typeToEncoder = map[uintptr]func(unsafe.Pointer)([]byte, error){}
   165  
   166  func Marshal(v interface{}) ([]byte, error) {
   167      iface := (*emptyInterface)(unsafe.Pointer(&v)
   168      typeptr := uintptr(iface.typ)
   169      if enc, exists := typeToEncoder[typeptr]; exists {
   170          return enc(iface.ptr)
   171      }
   172      ...
   173  }
   174  ```
   175  
   176  ※ In reality, `typeToEncoder` can be referenced by multiple goroutines, so exclusive control is required.
   177  
   178  ## Unique speed-up technique
   179  
   180  ## Encoder
   181  
   182  ### Do not escape arguments of `Marshal`
   183  
   184  `json.Marshal` and `json.Unmarshal` receive `interface{}` value and they perform type determination dynamically to process.
   185  In normal case, you need to use the `reflect` library to determine the type dynamically, but since `reflect.Type` is defined as `interface`, when you call the method of `reflect.Type`, The reflect's argument is escaped.
   186  
   187  Therefore, the arguments for `Marshal` and `Unmarshal` are always escaped to the heap.
   188  However, `go-json` can use the feature of `reflect.Type` while avoiding escaping.
   189  
   190  `reflect.Type` is defined as `interface`, but in reality `reflect.Type` is implemented only by the structure `rtype` defined in the `reflect` package.
   191  For this reason, to date `reflect.Type` is the same as `*reflect.rtype`.
   192  
   193  Therefore, by directly handling `*reflect.rtype`, which is an implementation of `reflect.Type`, it is possible to avoid escaping because it changes from `interface` to using `struct`.
   194  
   195  The technique for working with `*reflect.rtype` directly from `go-json` is implemented at [rtype.go](https://github.com/3JoB/go-json/blob/master/internal/runtime/rtype.go)
   196  
   197  Also, the same technique is cut out as a library ( https://github.com/3JoB/go-reflect )
   198  
   199  Initially this feature was the default behavior of `go-json`.
   200  But after careful testing, I found that I passed a large value to `json.Marshal()` and if the argument could not be assigned to the stack, it could not be properly escaped to the heap (a bug in the Go compiler).
   201  
   202  Therefore, this feature will be provided as an **optional** until this issue is resolved.
   203  
   204  To use it, add `NoEscape` like `MarshalNoEscape()`
   205  
   206  ### Encoding using opcode sequence
   207  
   208  I explained that you can use `typeptr` to call a pre-built process from type information.
   209  
   210  In other libraries, this dedicated process is processed by making it an function calling like anonymous function, but function calls are inherently slow processes and should be avoided as much as possible.
   211  
   212  Therefore, `go-json` adopted the Instruction-based execution processing system, which is also used to implement virtual machines for programming language.
   213  
   214  If it is the first type to encode, create the opcode ( instruction ) sequence required for encoding.
   215  From the second time onward, use `typeptr` to get the cached pre-built opcode sequence and encode it based on it. An example of the opcode sequence is shown below.
   216  
   217  ```go
   218  json.Marshal(struct{
   219      X int `json:"x"`
   220      Y string `json:"y"`
   221  }{X: 1, Y: "hello"})
   222  ```
   223  
   224  When encoding a structure like the one above, create a sequence of opcodes like this:
   225  
   226  ```
   227  - opStructFieldHead ( `{` )
   228  - opStructFieldInt ( `"x": 1,` )
   229  - opStructFieldString ( `"y": "hello"` )
   230  - opStructEnd ( `}` )
   231  - opEnd
   232  ```
   233  
   234  ※ When processing each operation, write the letters on the right.
   235  
   236  In addition, each opcode is managed by the following structure ( 
   237  Pseudo code ).
   238  
   239  ```go
   240  type opType int
   241  const (
   242      opStructFieldHead opType = iota
   243      opStructFieldInt
   244      opStructFieldStirng
   245      opStructEnd
   246      opEnd
   247  )
   248  type opcode struct {
   249      op opType
   250      key []byte
   251      next *opcode
   252  }
   253  ```
   254  
   255  The process of encoding using the opcode sequence is roughly implemented as follows.
   256  
   257  ```go
   258  func encode(code *opcode, b []byte, p unsafe.Pointer) ([]byte, error) {
   259      for {
   260          switch code.op {
   261          case opStructFieldHead:
   262              b = append(b, '{')
   263              code = code.next
   264          case opStructFieldInt:
   265              b = append(b, code.key...)
   266              b = appendInt((*int)(unsafe.Pointer(uintptr(p)+code.offset)))
   267              code = code.next
   268          case opStructFieldString:
   269              b = append(b, code.key...)
   270              b = appendString((*string)(unsafe.Pointer(uintptr(p)+code.offset)))
   271              code = code.next
   272          case opStructEnd:
   273              b = append(b, '}')
   274              code = code.next
   275          case opEnd:
   276              goto END
   277          }
   278      }
   279  END:
   280      return b, nil
   281  }
   282  ```
   283  
   284  In this way, the huge `switch-case` is used to encode by manipulating the linked list opcodes to avoid unnecessary function calls.
   285  
   286  ### Opcode sequence optimization
   287  
   288  One of the advantages of encoding using the opcode sequence is the ease of optimization.
   289  The opcode sequence mentioned above is actually converted into the following optimized operations and used.
   290  
   291  ```
   292  - opStructFieldHeadInt ( `{"x": 1,` )
   293  - opStructEndString ( `"y": "hello"}` )
   294  - opEnd
   295  ```
   296  
   297  It has been reduced from 5 opcodes to 3 opcodes !
   298  Reducing the number of opcodees means reducing the number of branches with `switch-case`.
   299  In other words, the closer the number of operations is to 1, the faster the processing can be performed.
   300  
   301  In `go-json`, optimization to reduce the number of opcodes itself like the above and it speeds up by preparing opcodes with optimized paths.
   302  
   303  ### Change recursive call from CALL to JMP
   304  
   305  Recursive processing is required during encoding if the type is defined recursively as follows:
   306  
   307  ```go
   308  type T struct {
   309      X int
   310      U *U
   311  }
   312  
   313  type U struct {
   314      T *T
   315  }
   316  
   317  b, err := json.Marshal(&T{
   318      X: 1,
   319      U: &U{
   320          T: &T{
   321              X: 2,
   322          },
   323      },
   324  })
   325  fmt.Println(string(b)) // {"X":1,"U":{"T":{"X":2,"U":null}}}
   326  ```
   327  
   328  In `go-json`, recursive processing is processed by the operation type of ` opStructFieldRecursive`.
   329  
   330  In this operation, after acquiring the opcode sequence used for recursive processing, the function is **not** called recursively as it is, but the necessary values ​​are saved by itself and implemented by moving to the next operation.
   331  
   332  The technique of implementing recursive processing with the `JMP` operation while avoiding the `CALL` operation is a famous technique for implementing a high-speed virtual machine.
   333  
   334  For more details, please refer to [the article](https://engineering.mercari.com/blog/entry/1599563768-081104c850) ( but Japanese only ).
   335  
   336  ### Dispatch by typeptr from map to slice
   337  
   338  When retrieving the data cached from the type information by `typeptr`, we usually use map.
   339  Map requires exclusive control, so use `sync.Map` for a naive implementation.
   340  
   341  However, this is slow, so it's a good idea to use the `atomic` package for exclusive control as implemented by `segmentio/encoding/json` ( https://github.com/segmentio/encoding/blob/master/json/codec.go#L41-L55 ).
   342  
   343  This implementation slows down the set instead of speeding up the get, but it works well because of the nature of the library, it encodes much more for the same type.
   344  
   345  However, as a result of profiling, I noticed that `runtime.mapaccess2` accounts for a significant percentage of the execution time. So I thought if I could change the lookup from map to slice.
   346  
   347  There is an API named `typelinks` defined in the `runtime` package that the `reflect` package uses internally.
   348  This allows you to get all the type information defined in the binary at runtime.
   349  
   350  The fact that all type information can be acquired means that by constructing slices in advance with the acquired total number of type information, it is possible to look up with the value of `typeptr` without worrying about out-of-range access.
   351  
   352  However, if there is too much type information, it will use a lot of memory, so by default we will only use this optimization if the slice size fits within **2Mib** .
   353  
   354  If this approach is not available, it will fall back to the `atomic` based process described above.
   355  
   356  If you want to know more, please refer to the implementation [here](https://github.com/3JoB/go-json/blob/master/internal/runtime/type.go#L36-L100)
   357  
   358  ## Decoder
   359  
   360  ### Dispatch by typeptr from map to slice
   361  
   362  Like the encoder, the decoder also uses typeptr to call the dedicated process.
   363  
   364  ### Faster termination character inspection using NUL character
   365  
   366  In order to decode, you have to traverse the input buffer character by position.
   367  At that time, if you check whether the buffer has reached the end, it will be very slow.
   368  
   369  `buf` : `[]byte` type variable. holds the string passed to the decoder
   370  `cursor` : `int64` type variable. holds the current read position
   371  
   372  ```go
   373  buflen := len(buf)
   374  for ; cursor < buflen; cursor++ { // compare cursor and buflen at all times, it is so slow.
   375      switch buf[cursor] {
   376      case ' ', '\n', '\r', '\t':
   377      }
   378  }
   379  ```
   380  
   381  Therefore, by adding the `NUL` (`\000`) character to the end of the read buffer as shown below, it is possible to check the termination character at the same time as other characters.
   382  
   383  ```go
   384  for {
   385      switch buf[cursor] {
   386      case ' ', '\n', '\r', '\t':
   387      case '\000':
   388          return nil
   389      }
   390      cursor++
   391  }
   392  ```
   393  
   394  ### Use Boundary Check Elimination
   395  
   396  Due to the `NUL` character optimization, the Go compiler does a boundary check every time, even though `buf[cursor]` does not cause out-of-range access.
   397  
   398  Therefore, `go-json` eliminates boundary check by fetching characters for hotspot by pointer operation. For example, the following code.
   399  
   400  ```go
   401  func char(ptr unsafe.Pointer, offset int64) byte {
   402  	return *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(offset)))
   403  }
   404  
   405  p := (*sliceHeader)(&unsafe.Pointer(buf)).data
   406  for {
   407      switch char(p, cursor) {
   408      case ' ', '\n', '\r', '\t':
   409      case '\000':
   410          return nil
   411      }
   412      cursor++
   413  }
   414  ```
   415  
   416  ### Checking the existence of fields of struct using Bitmaps
   417  
   418  I found by the profiling result, in the struct decode, lookup process for field was taking a long time.
   419  
   420  For example, consider decoding a string like `{"a":1,"b":2,"c":3}` into the following structure:
   421  
   422  ```go
   423  type T struct {
   424      A int `json:"a"`
   425      B int `json:"b"`
   426      C int `json:"c"`
   427  }
   428  ```
   429  
   430  At this time, it was found that it takes a lot of time to acquire the decoding process corresponding to the field from the field name as shown below during the decoding process.
   431  
   432  ```go
   433  fieldName := decodeKey(buf, cursor) // "a" or "b" or "c"
   434  decoder, exists := fieldToDecoderMap[fieldName] // so slow
   435  if exists {
   436      decoder(buf, cursor)
   437  } else {
   438      skipValue(buf, cursor)
   439  }
   440  ```
   441  
   442  To improve this process, `json-iterator/go` is optimized so that it can be branched by switch-case when the number of fields in the structure is 10 or less (switch-case is faster than map). However, there is a risk of hash collision because the value hashed by the FNV algorithm is used for conditional branching. Also, `gojay` processes this part at high speed by letting the library user yourself write `switch-case`.
   443  
   444  
   445  `go-json` considers and implements a new approach that is different from these. I call this **bitmap field optimization**.
   446  
   447  The range of values ​​per character can be represented by `[256]byte`. Also, if the number of fields in the structure is 8 or less, `int8` type can represent the state of each field.
   448  In other words, it has the following structure.
   449  
   450  - Base ( 8bit ): `00000000`
   451  - Key "a": `00000001` ( assign key "a" to the first bit )
   452  - Key "b": `00000010` ( assign key "b" to the second bit )
   453  - Key "c": `00000100` ( assign key "c" to the third bit )
   454  
   455  Bitmap structure is the following
   456  
   457  ```
   458          | key index(0) |
   459  ------------------------
   460   0      | 00000000     |
   461   1      | 00000000     |
   462  ~~      |              |
   463  97 (a)  | 00000001     |
   464  98 (b)  | 00000010     |
   465  99 (c)  | 00000100     |
   466  ~~      |              |
   467  255     | 00000000     |
   468  ```
   469  
   470  You can think of this as a Bitmap with a height of `256` and a width of the maximum string length in the field name.
   471  In other words, it can be represented by the following type .
   472  
   473  ```go
   474  [maxFieldKeyLength][256]int8
   475  ```
   476  
   477  When decoding a field character, check whether the corresponding character exists by referring to the pre-built bitmap like the following.
   478  
   479  ```go
   480  var curBit int8 = math.MaxInt8 // 11111111
   481  
   482  c := char(buf, cursor)
   483  bit := bitmap[keyIdx][c]
   484  curBit &= bit
   485  if curBit == 0 {
   486      // not found field
   487  }
   488  ```
   489  
   490  If `curBit` is not `0` until the end of the field string, then the string is
   491  You may have hit one of the fields.
   492  But the possibility is that if the decoded string is shorter than the field string, you will get a false hit.
   493  
   494  - input: `{"a":1}`
   495  ```go
   496  type T struct {
   497      X int `json:"abc"`
   498  }
   499  ```
   500  ※ Since `a` is shorter than `abc`, it can decode to the end of the field character without `curBit` being 0.
   501  
   502  Rest assured. In this case, it doesn't matter because you can tell if you hit by comparing the string length of `a` with the string length of `abc`.
   503  
   504  Finally, calculate the position of the bit where `1` is set and get the corresponding value, and you're done.
   505  
   506  Using this technique, field lookups are possible with only bitwise operations and access to slices.
   507  
   508  `go-json` uses a similar technique for fields with 9 or more and 16 or less fields. At this time, Bitmap is constructed as `[maxKeyLen][256]int16` type.
   509  
   510  Currently, this optimization is not performed when the maximum length of the field name is long (specifically, 64 bytes or more) in addition to the limitation of the number of fields from the viewpoint of saving memory usage.
   511  
   512  ### Others
   513  
   514  I have done a lot of other optimizations. I will find time to write about them. If you have any questions about what's written here or other optimizations, please visit the `#go-json` channel on `gophers.slack.com` .
   515  
   516  ## Reference
   517  
   518  Regarding the story of go-json, there are the following articles in Japanese only.
   519  
   520  - https://speakerdeck.com/goccy/zui-su-falsejsonraiburariwoqiu-mete
   521  - https://engineering.mercari.com/blog/entry/1599563768-081104c850/
   522  
   523  # Looking for Sponsors
   524  
   525  I'm looking for sponsors this library. This library is being developed as a personal project in my spare time. If you want a quick response or problem resolution when using this library in your project, please register as a [sponsor](https://github.com/sponsors/goccy). I will cooperate as much as possible. Of course, this library is developed as an MIT license, so you can use it freely for free.
   526  
   527  # License
   528  
   529  MIT