Skip to content

Latest commit

 

History

History
494 lines (387 loc) · 17.8 KB

README_ZH.md

File metadata and controls

494 lines (387 loc) · 17.8 KB

sonic-rs

Crates.io Documentation Website License Build Status

中文 | English

sonic-rs 是一个基于 SIMD 的高性能 JSON 库。它参考了其他开源库如 sonic_cppserde_jsonsonicsimdjsonrust-std 等。

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

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

有关优化的更多细节,请参见 performance_zh.md

对于 Golang 用户迁移 Rust 使用 sonic_rs, 请参考 for_Golang_user.md

要求/注意事项

  1. 支持 x86_64 或 aarch64,aarch64 的性能较低,需要优化。
  2. 需要 Rust nightly 版本,因为 sonic-rs 使用了 packed_simd 包。
  3. 在编译选项中开启 -C target-cpu=native

如何使用 sonic-rs

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

在 Cargo 依赖中添加 sonic-rs:

[dependencies]
sonic-rs = 0.2

功能

  1. JSON 与 Rust 结构体之间的序列化,基于兼容 serde_jsonserde
  2. JSON 与 document 之间的序列化,document是可变数据结构
  3. 从 JSON 中获取特定字段
  4. 将 JSON 解析为惰性迭代器
  5. 在默认情况下支持 RawValueNumberRawNumber(就像 Golang 的 JsonNumber)。
  6. 浮点数精度默认和 Rust 标准库对齐

基准测试

基准测试环境:

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

基准测试主要有两个方面:

  • 解析到结构体:定义的结构体和测试数据来自 json-benchmark

  • 解析到 document

序列化基准测试也是如此。

解析相关 benchmark 都开启了 UTF-8 校验,同时 serde-json 开启了 float_roundtrip feature, 以便解析浮点数具有足够精度,和 Rust 标准库对齐。

解析到结构体

基准测试将把 JSON 解析成 Rust 结构体,JSON 文本中没有未知字段。JSON 中的所有字段都被解析为结构体字段。

Sonic-rs 比 simd-json 更快,因为 simd-json (Rust) 首先将 JSON 解析成 tape,然后将 tape 解析成 Rust 结构体。Sonic-rs 直接将 JSON 解析成 Rust 结构体,没有临时数据结构。在 citm_catalog 案例中对 flamegraph 进行了分析。

cargo bench --bench deserialize_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]

解析到 document

该测试将把 JSON 解析成 document。由于以下几个原因,Sonic-rs 会看起来更快一些:

  • 如上所述,在 sonic-rs 中没有临时数据结构,例如 tape
  • Sonic-rs 为整个 document 使用内存区,从而减少内存分配、提高缓存友好性和可变性。
  • sonic-rs document中的 JSON 对象实际上是一个向量。Sonic-rs 不会构建 hashmap。

cargo bench --bench deserialize_value -- --quiet

twitter/sonic_rs_dom::from_slice
                        time:   [621.16 µs 624.89 µs 628.91 µs]
twitter/sonic_rs_dom::from_slice_unchecked
                        time:   [588.34 µs 594.28 µs 601.36 µs]
twitter/simd_json::slice_to_borrowed_value
                        time:   [1.3001 ms 1.3400 ms 1.3853 ms]
twitter/serde_json::from_slice
                        time:   [3.9263 ms 3.9822 ms 4.0463 ms]
twitter/serde_json::from_str
                        time:   [2.8608 ms 2.9187 ms 2.9907 ms]
twitter/simd_json::slice_to_owned_value
                        time:   [1.7870 ms 1.8044 ms 1.8230 ms]

citm_catalog/sonic_rs_dom::from_slice
                        time:   [1.8024 ms 1.8234 ms 1.8469 ms]
citm_catalog/sonic_rs_dom::from_slice_unchecked
                        time:   [1.7280 ms 1.7731 ms 1.8235 ms]
citm_catalog/simd_json::slice_to_borrowed_value
                        time:   [3.5792 ms 3.6082 ms 3.6386 ms]
citm_catalog/serde_json::from_slice
                        time:   [8.4606 ms 8.5654 ms 8.6896 ms]
citm_catalog/serde_json::from_str
                        time:   [9.3020 ms 9.4903 ms 9.6760 ms]
citm_catalog/simd_json::slice_to_owned_value
                        time:   [4.3144 ms 4.4268 ms 4.5604 ms]

canada/sonic_rs_dom::from_slice
                        time:   [5.1103 ms 5.1784 ms 5.2654 ms]
canada/sonic_rs_dom::from_slice_unchecked
                        time:   [4.8870 ms 4.9165 ms 4.9499 ms]
canada/simd_json::slice_to_borrowed_value
                        time:   [12.583 ms 12.866 ms 13.178 ms]
canada/serde_json::from_slice
                        time:   [17.054 ms 17.218 ms 17.414 ms]
canada/serde_json::from_str
                        time:   [17.140 ms 17.363 ms 17.614 ms]
canada/simd_json::slice_to_owned_value
                        time:   [12.351 ms 12.503 ms 12.666 ms]

序列化 document

cargo bench --bench serialize_value -- --quiet

在以下基准测试中,对于 twitter JSON,sonic-rs 看似更快。 因为 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]

序列化 Rust 结构体

cargo bench --bench serialize_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 中获取

cargo bench --bench get_from -- --quiet

基准测试是从 twitter JSON 中获取特定字段。

  • sonic-rs::get_unchecked_from_str: 不校验json
  • sonic-rs::get_from_str: 校验json
  • gjson::get_from_str: 不校验json

在 get_unchecked_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]

用法

对 Rust 类型解析/序列化

直接使用 DeserializeSerialize trait。

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 切片。

sonic-rs 提供了 getget_unchecked 两种接口。请注意,如果使用 unchecked 接口,需要保证 输入的JSON 是格式良好且合法的,否则可能返回非预期结果。

use sonic_rs::{get_from_str, pointer, JsonValue, PointerNode};

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

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

解析/序列化 document

在 sonic-rs 中,JSON 可以被解析未可修改的document。需要注意,document 是由 bump 分配器管理。建议将 document 转换为 Object/ObjectMut 或 Array/ArrayMut。这样能够确保强类型,同时在使用时可以对 allocator 无感知。

use sonic_rs::value::{dom_from_slice, Value};
use sonic_rs::PointerNode;
use sonic_rs::{pointer, JsonValue};
fn main() {
    let json = r#"{
        "name": "Xiaoming",
        "obj": {},
        "arr": [],
        "age": 18,
        "address": {
            "city": "Beijing"
        },
        "phones": [
            "+123456"
        ]
    }"#;

    let mut dom = dom_from_slice(json.as_bytes()).unwrap();
    // get the value from dom
    let root = dom.as_value();

    // 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 mut obj = dom.as_object_mut().unwrap();
    let value = Value::new_bool(true);
    obj.insert("inserted", value);
    assert!(obj.contains_key("inserted"));
}

JSON Iterator

将 JSON object 或 Array 解析为惰性迭代器。迭代器的 Item 是 LazyValueResult<LazyValue>

use bytes::Bytes;
use sonic_rs::{to_array_iter, JsonValue};

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 {
        // 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"
            );
        }
    }
}

JSON RawValue & Number & RawNumber

如果我们需要得到原始的 JSON 文本,可以使用 RawValue。 如果我们需要将 JSON 数字解析为 untyped number,可以使用 Number。 如果我们需要解析 JSON 数字时*不丢失精度,可以使用 RawNumber,它类似于 Golang 中的 JsonNumber。

详细示例可以在raw_value.rsjson_number.rs 中找到。

错误处理

sonic-rs的错误处理参考了 serde-json,同时加上了对错误位置的描述。

use sonic_rs::{from_slice, from_str, Deserialize};

fn main() {
    #[allow(dead_code)]
    #[derive(Debug, Deserialize)]
    struct Foo {
        a: Vec<i32>,
        c: String,
    }

    // deal with Eof errors
    let err = from_str::<Foo>("{\"a\": [").unwrap_err();
    assert!(err.is_eof());
    eprintln!("{}", err);
    // EOF while parsing at line 1 column 6

    //     {"a": [
    //     ......^
    assert_eq!(
        format!("{}", err),
        "EOF while parsing at line 1 column 6\n\n\t{\"a\": [\n\t......^\n"
    );

    // deal with Data errors
    let err = from_str::<Foo>("{ \"b\":[]}").unwrap_err();
    eprintln!("{}", err);
    assert!(err.is_data());
    // println as follows:
    // missing field `a` at line 1 column 8
    //
    //     { "b":[]}
    //     ........^
    assert_eq!(
        format!("{}", err),
        "missing field `a` at line 1 column 8\n\n\t{ \"b\":[]}\n\t........^\n"
    );

    // deal with Syntax errors
    let err = from_slice::<Foo>(b"{\"b\":\"\x80\"}").unwrap_err();
    eprintln!("{}", err);
    assert!(err.is_syntax());
    // println as follows:
    // Invalid UTF-8 characters in json at line 1 column 6
    //
    //     {"b":"�"}
    //     ......^...
    assert_eq!(
        format!("{}", err),
        "Invalid UTF-8 characters in json at line 1 column 6\n\n\t{\"b\":\"\"}\n\t......^...\n"
    );
}

常见问题

关于 UTF-8

sonic-rs 默认并不开启 utf-8 校验,这是为了性能做出的权衡。

  • 对于 from_slicedom_from_slice 接口,默认开启了 utf8 校验。如果用户确保是 utf-8, 也可以使用 from_slice_uncheckeddom_from_slice_unchecked

关于浮点数精度

sonic-rs 默认使用和 Rust 标准库一致的浮点数精度,无需像 serde-json 那样添加额外的 float_roundtrip feature 来保证浮点数精度。

如果想在解析浮点数时,做到精度无损失,例如 Golang JsonNumberserde-json arbitrary_precision,可以使用 RawNumber

致谢

Thanks the following open-source libraries. sonic-rs has some references to other open-source libraries like sonic_cpp, serde_json, sonic, simdjson, yyjson, rust-std and so on.

我们为了性能重写了来自 sonic-cpp/sonic/simdjson/yyjson 的许多 SIMD 算法。我们重用了来自 serde_json 的反/序列化代码,并修改了必要的部分以与 serde 高度兼容。我们重用了来自 rust-std 的部分浮点解析代码,使其结构更准确。

如何贡献

Please read CONTRIBUTING.md for information on contributing to sonic-rs.