#serde-json #json #parse-json #deserialize-json #json-object #json-parser #simd

sonic-rs

Sonic-rs 是一个基于 SIMD 的快速 Rust JSON 库

20 个版本

新增 0.3.11 2024 年 8 月 20 日
0.3.9 2024 年 7 月 26 日
0.3.4 2024 年 3 月 13 日
0.3.0 2023 年 12 月 26 日
0.2.4 2023 年 11 月 7 日

#70编码

Download history 224/week @ 2024-05-04 169/week @ 2024-05-11 339/week @ 2024-05-18 349/week @ 2024-05-25 310/week @ 2024-06-01 1263/week @ 2024-06-08 1157/week @ 2024-06-15 838/week @ 2024-06-22 1188/week @ 2024-06-29 4342/week @ 2024-07-06 2817/week @ 2024-07-13 3246/week @ 2024-07-20 3510/week @ 2024-07-27 3943/week @ 2024-08-03 3090/week @ 2024-08-10 3111/week @ 2024-08-17

14,263 每月下载量
32 个 crate(25 个直接使用) 中使用

Apache-2.0

3MB
18K SLoC

sonic-rs

Crates.io Documentation Website License Build Status

英文 | 中文

一个基于 SIMD 的快速 Rust JSON 库。它参考了一些其他开源库,如 sonic_cppserde_jsonsonicsimdjsonrust-std 等。

对于 Golang 用户使用 sonic_rs,请参阅 for_Golang_user.md

对于从 serde_json 迁移到 sonic_rs 的用户,可以查看 serdejson_compatibility

要求/注意

  1. 在 x86_64 或 aarch64 上更快,其他架构是回退,可能非常慢。

  2. 需要 Rust 夜间版本 现在支持稳定的 Rust。

  3. 请添加编译选项 -C target-cpu=native

快速使用 sonic-rs

为了确保 sonic-rs 使用 SIMD 指令,您需要添加 rustflags -C target-cpu=native 并在主机机器上编译。例如,可以在 Cargo 配置 中配置 Rust 标志。

Cargo.toml 中添加 sonic-rs

[dependencies]
sonic-rs = "0.3"

功能

  1. 将 Serde 编译到 Rust 结构体中,如 serde_jsonserde

  2. 解析/序列化 JSON 以用于无类型 sonic_rs::Value,它是可变的。

  3. 以闪电般的性能从 JSON 中获取特定字段。

  4. 以闪电般的性能将 JSON 用作懒数组或对象迭代器。

  5. 默认支持 LazyValueNumberRawNumber(类似于 Golang 的 JsonNumber)。

  6. 默认情况下,浮点数解析的精度与 Rust std 相同。

基准测试

sonic-rs 中的主要优化是使用 SIMD。然而,我们并不使用来自 simd-json 的两阶段 SIMD 算法。我们主要在以下场景中使用 SIMD:

  1. 解析/序列化长 JSON 字符串
  2. 解析浮点数的分数
  3. 从 JSON 获取特定元素或字段
  4. 解析 JSON 时跳过空白字符

更多关于优化的细节可以在 performance.md 中找到。

基准测试环境

Architecture:        x86_64
Model name:          Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz

AArch64 基准数据可以在 benchmark_aarch64.md 中找到。

基准测试

  • 反序列化结构体:将 JSON 反序列化为 Rust 结构体。定义的结构体和测试数据来自 json-benchmark

  • 反序列化无类型:将 JSON 反序列化为无类型文档

序列化基准测试的工作方式相反。

所有反序列化基准测试都启用了 UTF-8 验证,并在 serde-json 中启用了 float_roundtrip 以获得与 Rust std 相同的足够精度。

反序列化结构体

该基准测试将解析 JSON 并将其转换为 Rust 结构体,JSON 文本中没有未知字段。所有字段都解析为 JSON 中的结构体字段。

sonic-rs 比 simd-json 快,因为 simd-json(Rust)首先将 JSON 解析为 tape,然后解析 tape 并将其转换为 Rust 结构体。sonic-rs 直接将 JSON 解析为 Rust 结构体,没有临时数据结构。在 citm_catalog 案例中,flamegraph 已进行性能分析。

cargobench --benchdeserialize_struct ----quiet

twitter/sonic_rs::from_slice_unchecked
                        time:   [694.74 µs 707.83 µs 723.19 µs]
twitter/sonic_rs::from_slice
                        time:   [796.44 µs 827.74 µs 861.30 µs]
twitter/simd_json::from_slice
                        time:   [1.0615 ms 1.0872 ms 1.1153 ms]
twitter/serde_json::from_slice
                        time:   [2.2659 ms 2.2895 ms 2.3167 ms]
twitter/serde_json::from_str
                        time:   [1.3504 ms 1.3842 ms 1.4246 ms]

citm_catalog/sonic_rs::from_slice_unchecked
                        time:   [1.2271 ms 1.2467 ms 1.2711 ms]
citm_catalog/sonic_rs::from_slice
                        time:   [1.3344 ms 1.3671 ms 1.4050 ms]
citm_catalog/simd_json::from_slice
                        time:   [2.0648 ms 2.0970 ms 2.1352 ms]
citm_catalog/serde_json::from_slice
                        time:   [2.9391 ms 2.9870 ms 3.0481 ms]
citm_catalog/serde_json::from_str
                        time:   [2.5736 ms 2.6079 ms 2.6518 ms]

canada/sonic_rs::from_slice_unchecked
                        time:   [3.7779 ms 3.8059 ms 3.8368 ms]
canada/sonic_rs::from_slice
                        time:   [3.9676 ms 4.0212 ms 4.0906 ms]
canada/simd_json::from_slice
                        time:   [7.9582 ms 8.0932 ms 8.2541 ms]
canada/serde_json::from_slice
                        time:   [9.2184 ms 9.3560 ms 9.5299 ms]
canada/serde_json::from_str
                        time:   [9.0383 ms 9.2563 ms 9.5048 ms]

反序列化无类型

该基准测试将解析 JSON 并将其转换为文档。sonic-rs 由于以下原因似乎更快:

  • 如上所述,sonic-rs 中也没有临时数据结构。
  • sonic-rs 使用整个文档的内存区域,导致更少的内存分配,更好的缓存友好性,以及可变性。
  • sonic_rs::Value 中的 JSON 对象是一个数组。sonic-rs 不构建哈希表。

cargobench --benchdeserialize_value ----quiet

twitter/sonic_rs_dom::from_slice
                        time:   [550.95 µs 556.10 µs 562.89 µs]
twitter/sonic_rs_dom::from_slice_unchecked
                        time:   [525.97 µs 530.26 µs 536.06 µs]
twitter/serde_json::from_slice
                        time:   [3.7599 ms 3.8009 ms 3.8513 ms]
twitter/serde_json::from_str
                        time:   [2.8618 ms 2.8960 ms 2.9396 ms]
twitter/simd_json::slice_to_owned_value
                        time:   [1.7302 ms 1.7557 ms 1.7881 ms]
twitter/simd_json::slice_to_borrowed_value
                        time:   [1.1870 ms 1.1951 ms 1.2039 ms]

canada/sonic_rs_dom::from_slice
                        time:   [4.9060 ms 4.9568 ms 5.0213 ms]
canada/sonic_rs_dom::from_slice_unchecked
                        time:   [4.7858 ms 4.8728 ms 4.9803 ms]
canada/serde_json::from_slice
                        time:   [16.689 ms 16.980 ms 17.335 ms]
canada/serde_json::from_str
                        time:   [16.398 ms 16.640 ms 16.932 ms]
canada/simd_json::slice_to_owned_value
                        time:   [12.627 ms 12.846 ms 13.070 ms]
canada/simd_json::slice_to_borrowed_value
                        time:   [12.030 ms 12.164 ms 12.323 ms]

citm_catalog/sonic_rs_dom::from_slice
                        time:   [1.6657 ms 1.6981 ms 1.7341 ms]
citm_catalog/sonic_rs_dom::from_slice_unchecked
                        time:   [1.5109 ms 1.5253 ms 1.5424 ms]
citm_catalog/serde_json::from_slice
                        time:   [8.1618 ms 8.2566 ms 8.3653 ms]
citm_catalog/serde_json::from_str
                        time:   [7.8652 ms 8.0706 ms 8.3074 ms]
citm_catalog/simd_json::slice_to_owned_value
                        time:   [3.9834 ms 4.0325 ms 4.0956 ms]
citm_catalog/simd_json::slice_to_borrowed_value
                        time:   [3.3196 ms 3.3433 ms 3.3689 ms]

序列化无类型

cargobench --benchserialize_value ----quiet

我们将文档序列化为字符串。在以下基准测试中,sonic-rs 在 twitter JSON 中似乎更快。该 twitter JSON 包含许多长 JSON 字符串,这与 sonic-rs 的 SIMD 优化非常匹配。

twitter/sonic_rs::to_string
                        time:   [380.90 µs 390.00 µs 400.38 µs]
twitter/serde_json::to_string
                        time:   [788.98 µs 797.34 µs 807.69 µs]
twitter/simd_json::to_string
                        time:   [965.66 µs 981.14 µs 998.08 µs]

citm_catalog/sonic_rs::to_string
                        time:   [805.85 µs 821.99 µs 841.06 µs]
citm_catalog/serde_json::to_string
                        time:   [1.8299 ms 1.8880 ms 1.9498 ms]
citm_catalog/simd_json::to_string
                        time:   [1.7356 ms 1.7636 ms 1.7972 ms]

canada/sonic_rs::to_string
                        time:   [6.5808 ms 6.7082 ms 6.8570 ms]
canada/serde_json::to_string
                        time:   [6.4800 ms 6.5747 ms 6.6893 ms]
canada/simd_json::to_string
                        time:   [7.3751 ms 7.5690 ms 7.7944 ms]

序列化结构体

cargobench --benchserialize_struct ----quiet

解释如上所述。

twitter/sonic_rs::to_string
                        time:   [434.03 µs 448.25 µs 463.97 µs]
twitter/simd_json::to_string
                        time:   [506.21 µs 515.54 µs 526.35 µs]
twitter/serde_json::to_string
                        time:   [719.70 µs 739.97 µs 762.69 µs]

canada/sonic_rs::to_string
                        time:   [4.6701 ms 4.7481 ms 4.8404 ms]
canada/simd_json::to_string
                        time:   [5.8072 ms 5.8793 ms 5.9625 ms]
canada/serde_json::to_string
                        time:   [4.5708 ms 4.6281 ms 4.6967 ms]

citm_catalog/sonic_rs::to_string
                        time:   [624.86 µs 629.54 µs 634.57 µs]
citm_catalog/simd_json::to_string
                        time:   [624.10 µs 633.55 µs 644.78 µs]
citm_catalog/serde_json::to_string
                        time:   [802.10 µs 814.15 µs 828.10 µs]

从 JSON 获取

cargobench --benchget_from ----quiet

该基准测试是从 twitter.json 获取特定字段。

  • sonic-rs::get_unchecked_from_str: 不验证
  • sonic-rs::get_from_str: 验证
  • gjson::get_from_str: 不验证

sonic-rs 利用 SIMD 在未检查的情况下快速跳过不必要的字段,从而提高性能。

twitter/sonic-rs::get_unchecked_from_str
                        time:   [75.671 µs 76.766 µs 77.894 µs]
twitter/sonic-rs::get_from_str
                        time:   [430.45 µs 434.62 µs 439.43 µs]
twitter/gjson::get_from_str
                        time:   [359.61 µs 363.14 µs 367.19 µs]

使用

将 Serde 序列化为 Rust 类型

直接使用 DeserializeSerialize 特性。

use sonic_rs::{Deserialize, Serialize}; 
// sonic-rs re-exported them from serde
// or use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

fn main() {
    let data = r#"{
  "name": "Xiaoming",
  "age": 18,
  "phones": [
    "+123456"
  ]
}"#;
    let p: Person = sonic_rs::from_str(data).unwrap();
    assert_eq!(p.age, 18);
    assert_eq!(p.name, "Xiaoming");
    let out = sonic_rs::to_string_pretty(&p).unwrap();
    assert_eq!(out, data);
}

从 JSON 获取字段

使用 pointer 路径从 JSON 获取特定字段。返回值是一个 LazyValue,它是原始有效 JSON 切片的包装器。

我们提供了 getget_unchecked API。应该在有效的 JSON 中使用 get_unchecked API,否则可能会返回意外的结果。

use sonic_rs::JsonValueTrait;
use sonic_rs::{get, get_unchecked, pointer};

fn main() {
    let path = pointer!["a", "b", "c", 1];
    let json = r#"
        {"u": 123, "a": {"b" : {"c": [null, "found"]}}}
    "#;
    let target = unsafe { get_unchecked(json, &path).unwrap() };
    assert_eq!(target.as_raw_str(), r#""found""#);
    assert_eq!(target.as_str().unwrap(), "found");

    let target = get(json, &path);
    assert_eq!(target.as_str().unwrap(), "found");
    assert_eq!(target.unwrap().as_raw_str(), r#""found""#);

    let path = pointer!["a", "b", "c", "d"];
    let json = r#"
        {"u": 123, "a": {"b" : {"c": [null, "found"]}}}
    "#;
    // not found from json
    let target = get(json, &path);
    assert!(target.is_err());
}

解析和序列化为无类型值

将 JSON 解析为 sonic_rs::Value

use sonic_rs::{from_str, json};
use sonic_rs::JsonValueMutTrait;
use sonic_rs::{pointer, JsonValueTrait, Value};

fn main() {
    let json = r#"{
        "name": "Xiaoming",
        "obj": {},
        "arr": [],
        "age": 18,
        "address": {
            "city": "Beijing"
        },
        "phones": [
            "+123456"
        ]
    }"#;

    let mut root: Value = from_str(json).unwrap();

    // get key from value
    let age = root.get("age").as_i64();
    assert_eq!(age.unwrap_or_default(), 18);

    // get by index
    let first = root["phones"][0].as_str().unwrap();
    assert_eq!(first, "+123456");

    // get by pointer
    let phones = root.pointer(&pointer!["phones", 0]);
    assert_eq!(phones.as_str().unwrap(), "+123456");

    // convert to mutable object
    let obj = root.as_object_mut().unwrap();
    obj.insert(&"inserted", true);
    assert!(obj.contains_key(&"inserted"));

    let mut object = json!({ "A": 65, "B": 66, "C": 67 });
    *object.get_mut("A").unwrap() = json!({
        "code": 123,
        "success": false,
        "payload": {}
    });

    let mut val = json!(["A", "B", "C"]);
    *val.get_mut(2).unwrap() = json!("D");

    // serialize
    assert_eq!(serde_json::to_string(&val).unwrap(), r#"["A","B","D"]"#);
}

JSON 迭代器

将对象或数组 JSON 解析为懒迭代器。

use bytes::Bytes;
use faststr::FastStr;
use sonic_rs::JsonValueTrait;
use sonic_rs::{to_array_iter, to_object_iter_unchecked};
fn main() {
    let json = Bytes::from(r#"[1, 2, 3, 4, 5, 6]"#);
    let iter = to_array_iter(&json);
    for (i, v) in iter.enumerate() {
        assert_eq!(i + 1, v.as_u64().unwrap() as usize);
    }

    let json = Bytes::from(r#"[1, 2, 3, 4, 5, 6"#);
    let iter = to_array_iter(&json);
    for elem in iter {
        // do something for each elem

        // deal with errors when invalid json
        if elem.is_err() {
            assert_eq!(
                elem.err().unwrap().to_string(),
                "Expected this character to be either a ',' or a ']' while parsing at line 1 column 17"
            );
        }
    }

    let json = FastStr::from(r#"{"a": null, "b":[1, 2, 3]}"#);
    let iter = unsafe { to_object_iter_unchecked(&json) };
    for ret in iter {
        // deal with errors
        if ret.is_err() {
            println!("{}", ret.unwrap_err());
            return;
        }

        let (k, v) = ret.unwrap();
        if k == "a" {
            assert!(v.is_null());
        } else if k == "b" {
            let iter = to_array_iter(v.as_raw_str());
            for (i, v) in iter.enumerate() {
                assert_eq!(i + 1, v.as_u64().unwrap() as usize);
            }
        }
    }
}

JSON 懒值 & 数字 & 原始数字

如果我们需要将 JSON 值解析为原始字符串,我们可以使用 LazyValue

如果我们需要将 JSON 数字解析为无类型类型,我们可以使用 Number

如果我们需要解析 JSON 数字而不丢失精度,我们可以使用 RawNumber。它类似于 Golang 中的 encoding/json.Number,也可以从 JSON 字符串中解析。

详细示例请参见 raw_value.rsjson_number.rs

错误处理

Sonic 的错误遵循 serde-json,并在错误位置周围有显示,示例在 handle_error.rs

常见问题解答

关于 UTF-8

默认情况下,sonic-rs 启用了 UTF-8 验证,除了 xx_unchecked API。

关于浮点精度

默认情况下,sonic-rs 使用与 Rust 标准库一致的浮点精度,无需添加额外的 float_roundtrip 功能来确保浮点精度。

如果您希望在解析浮点数时达到无损精度,例如 Golang encoding/json.Numberserde-json arbitrary_precision,您可以使用 sonic_rs::RawNumber

致谢

感谢以下开源库。sonic-rs 引用了其他开源库,如 sonic_cppserde_jsonsonicsimdjsonyyjsonrust-std 等。

我们重写了 sonic-cpp/sonic/simdjson/yyjson 中的许多 SIMD 算法以提高性能。我们重用了 de/ser 代码,并从 serde_json 中修改了必要的部分,以提高与 serde 的高兼容性。我们还重用了来自 rust-std 的关于浮点解析的部分代码,使其更准确。

贡献

有关为 sonic-rs 贡献的信息,请参阅 CONTRIBUTING.md

依赖项

~1.6–7MB
~43K SLoC