sonic

repository·main·Indexed 27 days ago

https://github.com/bytedance/sonic

A high-performance JSON library for Go that utilizes JIT and SIMD acceleration for fast serialization and deserialization. It features runtime object binding, a complete set of APIs for JSON manipulation via ast.Node, and streaming IO support. Sonic provides multiple configuration profiles (ConfigDefault, ConfigStd, ConfigFastest) to balance speed and compatibility with encoding/json.

Tokens
8.1K
Snippets
17
Records
45
Agent score
92%

What's inside sonic

  1. Overview of Sonic Features

    main

    Sonic is a high-performance JSON serialization and deserialization library for Go. It is accelerated using JIT (just-in-time compiling) and SIMD (single-instruction-multiple-data) to achieve high throughput. Key features include:

    • Runtime object binding without the need for code generation.
    • A complete set of APIs for manipulating JSON values.
    • Extremely high performance across various JSON sizes and usage scenarios.
  2. Sonic Features and Performance

    main

    Sonic provides high-speed JSON operations with the following characteristics:

    • Runtime Object Binding: Supports binding without the need for code generation.
    • Complete API: Provides a comprehensive set of JSON manipulation APIs.
    • Performance: Optimized for all JSON sizes and use cases, including small, medium (13kB, 300+ keys), and large (635kB, 10,000+ keys) payloads.
  3. System Requirements for Sonic

    main

    Before using Sonic, ensure your environment meets the following requirements:

    • Go Version: 1.18 to 1.26.
      • Important: Go 1.24.0 is currently not supported due to a known issue. To use Go 1.24.0, you must pass the build flag -ldflags="-checklinkname=0".
    • Operating Systems: Linux, MacOS, or Windows.
    • CPU Architecture:
      • AMD64
      • ARM64 (requires Go 1.20 or higher)
  4. Use PretouchMany() to prevent JIT-related latency or OOM

    main

    Sonic uses a JIT assembler that compiles schemas at runtime. For very large schemas or latency-sensitive applications, the first-hit compilation can cause request timeouts or Out-of-Memory (OOM) errors. To mitigate this, use sonic.PretouchMany() during application initialization to pre-compile the required types.

    You can control the compilation depth using:

    • option.WithCompileRecursiveDepth(depth): Sets the recursion depth for deeply nested types.
    • option.WithCompileMaxInlineDepth(depth): Sets the maximum inline depth to reduce compilation time for large structs.
    import (
        "reflect"
        "github.com/bytedance/sonic"
        "github.com/bytedance/sonic/option"
    )
    
    func init() {
        var v1 HugeStruct1
        var v2 HugeStruct2
    
        // For most large types (nesting depth <= option.DefaultMaxInlineDepth)
        sonic.PretouchMany([]reflect.Type{reflect.TypeOf(v1), reflect.TypeOf(v2)},
            // If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
            // you can set more recursive loops in Pretouch for fully sufficient JIT.
            option.WithCompileRecursiveDepth(loop),
            // For a large struct, try to set a smaller depth to reduce compiling time.
            option.WithCompileMaxInlineDepth(depth),
        )
    }
  5. Optimize partial JSON parsing with ast.Node

    main

    For scenarios where you only need specific parts of a JSON document, combine Get() with Unmarshal() or use ast.Node as a generic container instead of map or interface{}. ast.Node uses an array-based storage that is more efficient for insertion and scanning than maps, and it supports lazy loading (parsing values only on demand).

    Concurrency Note: ast.Node is not inherently thread-safe due to its lazy loading design. To use a node concurrently, you must call Node.Load() or Node.LoadAll() first.

    import "github.com/bytedance/sonic"
    
    // Example: Combining Get and Unmarshal for partial schema
    node, err := sonic.GetFromString(_TwitterJson, "statuses", 3, "user")
    var user User
    err = sonic.UnmarshalString(node.Raw(), &user)
    
    // Example: Using ast.Node as a generic container
    root, err := sonic.GetFromString(_TwitterJson)
    user := root.GetByPath("statuses", 3, "user")
    err = user.Check()
    // err = user.LoadAll() // Call this to make 'user' safe for concurrent use
    go someFunc(user)
  6. Use UnmarshalString and GetFromString for zero-copy string handling

    main
    While Sonic provides []byte APIs for compatibility with encoding/json, converting strings to byte slices can incur performance costs for very large JSON. For better performance when your source data is already a string, use UnmarshalString() and GetFromString(). Additionally, use MarshalString() for encoding to allow safe zero-copy type conversion of the resulting JSON bytes.
  7. Pre-warm JIT with PretouchMany() to prevent timeouts

    main

    Sonic uses golang-asm as a JIT assembler. Running large JSON patterns for the first time at runtime can cause request timeouts or process memory overflows due to compilation overhead. To improve stability in latency-sensitive applications, run sonic.PretouchMany() with your target types before calling Marshal() or Unmarshal().

    import (
        "reflect"
        "github.com/bytedance/sonic"
        "github.com/bytedance/sonic/option"
    )
    
    func init() {
        var v1 HugeStruct1
        var v2 HugeStruct2
    
        // For most large types (nesting depth <= option.DefaultMaxInlineDepth)
        sonic.PretouchMany([]reflect.Type{reflect.TypeOf(v1), reflect.TypeOf(v2)},
            // If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
            // you can set more recursive loops in Pretouch for fully sufficient JIT.
            option.WithCompileRecursiveDepth(loop),
            // For a large struct, try to set a smaller depth to reduce compiling time.
            option.WithCompileMaxInlineDepth(depth),
        )
    }
  8. Use ast.Visitor for maximum performance in generic parsing

    main
    While ast.Node is a good generic container for partial parsing, it is still an intermediate representation. For extreme performance requirements where you want to parse JSON directly into your custom types without intermediate steps, use ast.Visitor. Note that ast.Visitor is a complex API that requires manual implementation of the visitor pattern and careful management of the tree hierarchy.
  9. Optimize string and []byte passing

    main

    To avoid the performance penalty of string-to-byte copying when handling large JSON data:

    • Use UnmarshalString() and GetFromString() when your source data is a string.
    • Use MarshalString() for convenient encoding of JSON bytes, as Sonic's output is always unique and safe for this purpose.
    • If your source is a []byte and you can safely perform a no-copy cast, use the specialized APIs to avoid the overhead of encoding/json alignment.
  10. Use Streaming IO for Encoding and Decoding

    main

    To reduce memory consumption and handle multiple JSON values, Sonic supports streaming via io.Reader and io.Writer.

    • Encoder: Use sonic.ConfigDefault.NewEncoder(w) to encode objects into a writer. You can call Encode multiple times to stream multiple JSON objects.
    • Decoder: Use sonic.ConfigDefault.NewDecoder(r) to decode from a reader. You can call Decode multiple times to process a stream of JSON objects.
    // Encoder example
    var o1 = map[string]interface{}{
        "a": "b",
    }
    var o2 = 1
    var w = bytes.NewBuffer(nil)
    var enc = sonic.ConfigDefault.NewEncoder(w)
    enc.Encode(o1)
    enc.Encode(o2)
    
    // Decoder example
    var o =  map[string]interface{}{}
    var r = strings.NewReader(`{"a":"b"}{"1":"2"}`)
    var dec = sonic.ConfigDefault.NewDecoder(r)
    dec.Decode(&o)
    dec.Decode(&o)
  11. Control string copying behavior in Decoder

    main
    By default, when decoding strings that do not contain escape characters, Sonic references the original JSON buffer instead of copying it to a new buffer. While this improves CPU performance, it can lead to higher memory usage because the entire JSON buffer is retained in memory as long as the decoded object is in use. To force a copy and reduce memory retention, use the decoder.CopyString() option.
  12. Install and Configure Sonic

    main

    Sonic is a high-performance JSON serialization/deserialization library accelerated by JIT (Just-In-Time compilation) and SIMD.

    Requirements

    • Go: 1.18 to 1.26
      • Note for Go 1.24.0: Due to a known issue, Sonic is unavailable on Go 1.24.0. You must either upgrade to a higher Go version or use the following build flag: --ldflags="-checklinkname=0"
    • OS: Linux, MacOS, or Windows
    • CPU Architecture:
      • AMD64
      • ARM64 (requires Go 1.20 or higher)