mirror of
https://github.com/nlohmann/json.git
synced 2026-11-04 19:59:23 +00:00
docs: compare json_document with Glaze, sonic, RapidJSON and Rust parsers
Extends the comparison in "How 3.13.0 got faster" from yyjson, simdjson and Boost.JSON to eleven engines: sonic-cpp, RapidJSON and Glaze (generic DOM, lazy_json, reading into structs) in C++, sonic-rs, simd-json and serde_json in Rust. Measured on an Apple M1 Max (Apple clang 21, Rust 1.99) and an x86-64 Haswell server (GCC 13, Clang 18). Every engine must return the same values, string bytes and bit-identical number sums before it is timed. The post now answers whether json_document is the fastest parser: it is the fastest document parser at parse+dump everywhere and at parsing on Apple silicon, ties with simdjson's DOM on x86-64, and loses to simdjson On-Demand and Glaze structs when only a few fields are read. tests/benchmarks/json_view/others holds the programs (bench_all.cpp, the Rust crate with its Cargo.lock), summarize.py and the pinned versions and checksums. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
9 files changed
+1987
-117
No files matched your search
@@ -0,0 +1,172 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Same workloads and method as bench_all.cpp: parse (build + free), traverse (parse, then visit every
|
||||
// value, convert every number, touch every string and key), parse+dump; every engine interleaved, an
|
||||
// untimed call before each timed one, best round reported.
|
||||
// simd-json parses in place, so its timings include copying the input into a mutable buffer.
|
||||
use simd_json::prelude::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
struct Stats {
|
||||
num: f64,
|
||||
str_: usize,
|
||||
nodes: usize,
|
||||
}
|
||||
|
||||
fn walk_simd(v: &simd_json::BorrowedValue, st: &mut Stats) {
|
||||
st.nodes += 1;
|
||||
if let Some(o) = v.as_object() {
|
||||
for (k, x) in o.iter() {
|
||||
st.str_ += k.len();
|
||||
walk_simd(x, st);
|
||||
}
|
||||
} else if let Some(a) = v.as_array() {
|
||||
for x in a.iter() {
|
||||
walk_simd(x, st);
|
||||
}
|
||||
} else if let Some(s) = v.as_str() {
|
||||
st.str_ += s.len();
|
||||
} else if v.is_u64() {
|
||||
st.num += v.as_u64().unwrap() as f64;
|
||||
} else if v.is_i64() {
|
||||
st.num += v.as_i64().unwrap() as f64;
|
||||
} else if v.is_f64() {
|
||||
st.num += v.as_f64().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_sonic(v: &sonic_rs::Value, st: &mut Stats) {
|
||||
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
|
||||
st.nodes += 1;
|
||||
if let Some(o) = v.as_object() {
|
||||
for (k, x) in o.iter() {
|
||||
st.str_ += k.len();
|
||||
walk_sonic(x, st);
|
||||
}
|
||||
} else if let Some(a) = v.as_array() {
|
||||
for x in a.iter() {
|
||||
walk_sonic(x, st);
|
||||
}
|
||||
} else if let Some(s) = v.as_str() {
|
||||
st.str_ += s.len();
|
||||
} else if v.is_number() {
|
||||
if let Some(u) = v.as_u64() {
|
||||
st.num += u as f64;
|
||||
} else if let Some(i) = v.as_i64() {
|
||||
st.num += i as f64;
|
||||
} else {
|
||||
st.num += v.as_f64().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_serde(v: &serde_json::Value, st: &mut Stats) {
|
||||
st.nodes += 1;
|
||||
match v {
|
||||
serde_json::Value::Object(o) => {
|
||||
for (k, x) in o.iter() {
|
||||
st.str_ += k.len();
|
||||
walk_serde(x, st);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(a) => {
|
||||
for x in a.iter() {
|
||||
walk_serde(x, st);
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => st.str_ += s.len(),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(u) = n.as_u64() {
|
||||
st.num += u as f64;
|
||||
} else if let Some(i) = n.as_i64() {
|
||||
st.num += i as f64;
|
||||
} else {
|
||||
st.num += n.as_f64().unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn same(a: &Stats, b: &Stats) -> bool {
|
||||
a.nodes == b.nodes && a.str_ == b.str_ && (a.num - b.num).abs() <= 1e-9 * a.num.abs()
|
||||
}
|
||||
|
||||
static mut SINK: f64 = 0.0;
|
||||
fn sink(x: f64) {
|
||||
unsafe { std::ptr::write_volatile(std::ptr::addr_of_mut!(SINK), x) }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let t = format!("{}/", args[1]);
|
||||
let rounds: usize = args.get(2).map(|r| r.parse().unwrap()).unwrap_or(30);
|
||||
let mut docs: Vec<(String, String, usize)> = Vec::new();
|
||||
for f in ["nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"] {
|
||||
let name = f.split('/').nth(1).unwrap().trim_end_matches(".json").to_string();
|
||||
docs.push((name, std::fs::read_to_string(format!("{}{}", t, f)).unwrap(), 1));
|
||||
}
|
||||
let tw: serde_json::Value = serde_json::from_str(&docs[0].1).unwrap();
|
||||
docs.push(("status".to_string(), serde_json::to_string(&tw["statuses"][0]).unwrap(), 200));
|
||||
|
||||
for (name, text, _) in &docs {
|
||||
let mut r = Stats::default();
|
||||
walk_serde(&serde_json::from_str(text).unwrap(), &mut r);
|
||||
let mut a = Stats::default();
|
||||
let mut buf = text.as_bytes().to_vec();
|
||||
walk_simd(&simd_json::to_borrowed_value(&mut buf).unwrap(), &mut a);
|
||||
let mut b = Stats::default();
|
||||
walk_sonic(&sonic_rs::from_str(text).unwrap(), &mut b);
|
||||
// sonic-rs keeps document order: its sum is compared bit for bit by the caller with json_view's
|
||||
println!("check {:<13} simd-json:{} sonic-rs:{} serde_json:OK (nodes {} str {} num {:.17e} sonic-num {:.17e})",
|
||||
name, if same(&r, &a) { "OK" } else { "MISMATCH" }, if same(&r, &b) { "OK" } else { "MISMATCH" }, r.nodes, r.str_, r.num, b.num);
|
||||
}
|
||||
|
||||
for (name, s, batch) in &docs {
|
||||
type Eng<'a> = (&'static str, Box<dyn Fn() + 'a>);
|
||||
let workloads: Vec<(&str, Vec<Eng>)> = vec![
|
||||
("parse", vec![
|
||||
("simd-json", Box::new(|| { let mut b = s.as_bytes().to_vec(); let v = simd_json::to_borrowed_value(&mut b).unwrap(); sink(v.is_object() as u8 as f64); })),
|
||||
("sonic-rs", Box::new(|| { let v: sonic_rs::Value = sonic_rs::from_str(s).unwrap(); sink(sonic_rs::JsonValueTrait::is_object(&v) as u8 as f64); })),
|
||||
("serde_json", Box::new(|| { let v: serde_json::Value = serde_json::from_str(s).unwrap(); sink(v.is_object() as u8 as f64); })),
|
||||
]),
|
||||
("traverse", vec![
|
||||
("simd-json", Box::new(|| { let mut b = s.as_bytes().to_vec(); let v = simd_json::to_borrowed_value(&mut b).unwrap(); let mut st = Stats::default(); walk_simd(&v, &mut st); sink(st.num); })),
|
||||
("sonic-rs", Box::new(|| { let v: sonic_rs::Value = sonic_rs::from_str(s).unwrap(); let mut st = Stats::default(); walk_sonic(&v, &mut st); sink(st.num); })),
|
||||
("serde_json", Box::new(|| { let v: serde_json::Value = serde_json::from_str(s).unwrap(); let mut st = Stats::default(); walk_serde(&v, &mut st); sink(st.num); })),
|
||||
]),
|
||||
("parse+dump", vec![
|
||||
("simd-json", Box::new(|| { let mut b = s.as_bytes().to_vec(); let v = simd_json::to_borrowed_value(&mut b).unwrap(); let o = simd_json::to_string(&v).unwrap(); sink(o.len() as f64); })),
|
||||
("sonic-rs", Box::new(|| { let v: sonic_rs::Value = sonic_rs::from_str(s).unwrap(); let o = sonic_rs::to_string(&v).unwrap(); sink(o.len() as f64); })),
|
||||
("serde_json", Box::new(|| { let v: serde_json::Value = serde_json::from_str(s).unwrap(); let o = serde_json::to_string(&v).unwrap(); sink(o.len() as f64); })),
|
||||
]),
|
||||
];
|
||||
for (wl, engines) in &workloads {
|
||||
let r = if s.len() > 10_000_000 { std::cmp::max(3, rounds / 5) } else { rounds };
|
||||
let mut best = vec![f64::MAX; engines.len()];
|
||||
for _ in 0..r {
|
||||
for (k, (_, f)) in engines.iter().enumerate() {
|
||||
f();
|
||||
let t0 = Instant::now();
|
||||
for _ in 0..*batch {
|
||||
f();
|
||||
}
|
||||
let ns = t0.elapsed().as_nanos() as f64 / *batch as f64;
|
||||
best[k] = best[k].min(ns);
|
||||
}
|
||||
}
|
||||
print!("{:<13} {:<11}", name, wl);
|
||||
for (k, (n, _)) in engines.iter().enumerate() {
|
||||
print!(" {} {:.1}us", n, best[k] / 1e3);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user