asmjson 0.2.6

A fast JSON parser using AVX-512/AVX2/SWAR classifiers
Documentation
#[cfg(target_arch = "x86_64")]
use asmjson::parse_to_dom_zmm;
#[cfg(target_arch = "x86_64")]
use asmjson::parse_with_zmm;
use asmjson::sax::Sax;
use asmjson::{DomEntryKind, parse_to_dom};
use criterion::{Criterion, Throughput, criterion_group, criterion_main};

// ---------------------------------------------------------------------------
// SAX writer — sums key and string byte lengths without building a tape
// ---------------------------------------------------------------------------

/// A [`Sax`] writer that accumulates the total byte length of every string
/// value and object key.  Allocates nothing; suitable for benchmarking the
/// pure parse cost of the SAX path.
struct LenSumWriter {
    total: usize,
}

impl LenSumWriter {
    fn new() -> Self {
        Self { total: 0 }
    }
}

impl<'src> Sax<'src> for LenSumWriter {
    type Output = usize;
    fn null(&mut self) {}

    fn bool_val(&mut self, _v: bool) {}

    fn number(&mut self, _s: &'src str) {}
    fn string(&mut self, s: &'src str) {
        self.total += s.len();
    }
    fn escaped_string(&mut self, s: &str) {
        self.total += s.len();
    }
    fn key(&mut self, s: &'src str) {
        self.total += s.len();
    }
    fn escaped_key(&mut self, s: &str) {
        self.total += s.len();
    }
    fn start_object(&mut self) {}

    fn end_object(&mut self) {}

    fn start_array(&mut self) {}

    fn end_array(&mut self) {}
    fn finish(self) -> Option<usize> {
        Some(self.total)
    }
}

// ---------------------------------------------------------------------------
// Post-parse traversal helpers
// ---------------------------------------------------------------------------

/// Sum the lengths of every String and Key entry in a tape.
#[inline]
fn dom_sum_lens(tape: &asmjson::Dom<'_>) -> usize {
    tape.entries
        .iter()
        .map(|e| match e.kind() {
            DomEntryKind::String | DomEntryKind::Key => e.as_string().map_or(0, |s| s.len()),
            _ => 0,
        })
        .sum()
}

/// Recursively sum the lengths of every string value and object key in a
/// serde_json Value tree.
fn serde_sum_lens(v: &serde_json::Value) -> usize {
    match v {
        serde_json::Value::String(s) => s.len(),
        serde_json::Value::Object(map) => map
            .iter()
            .map(|(k, val)| k.len() + serde_sum_lens(val))
            .sum(),
        serde_json::Value::Array(arr) => arr.iter().map(serde_sum_lens).sum(),
        _ => 0,
    }
}

/// Recursively sum the lengths of every string value and object key in a
/// sonic-rs Value tree.
fn sonic_sum_lens(v: &sonic_rs::Value) -> usize {
    use sonic_rs::{JsonContainerTrait, JsonValueTrait};
    if let Some(s) = v.as_str() {
        return s.len();
    }
    if let Some(obj) = v.as_object() {
        return obj
            .iter()
            .map(|(k, val): (&str, &sonic_rs::Value)| k.len() + sonic_sum_lens(val))
            .sum();
    }
    if let Some(arr) = v.as_array() {
        return arr
            .iter()
            .map(|val: &sonic_rs::Value| sonic_sum_lens(val))
            .sum();
    }
    0
}

/// Recursively sum the lengths of every string value and object key in a
/// simd-json BorrowedValue tree.
fn simd_sum_lens(v: &simd_json::BorrowedValue<'_>) -> usize {
    use simd_json::BorrowedValue;
    match v {
        BorrowedValue::String(s) => s.len(),
        BorrowedValue::Object(map) => map
            .iter()
            .map(|(k, val)| k.len() + simd_sum_lens(val))
            .sum(),
        BorrowedValue::Array(arr) => arr.iter().map(simd_sum_lens).sum(),
        _ => 0,
    }
}

// ---------------------------------------------------------------------------
// Data generators
// ---------------------------------------------------------------------------

/// ~10 MiB JSON array of strings.
///
/// Each element is a 95-character ASCII string (printable, no quotes/backslashes).
/// Enough elements are emitted to reach the target size.
fn gen_string_array(target_bytes: usize) -> String {
    // Element template: "abcdefghij..." (95 visible ASCII chars), plus `,"` overhead.
    let value: String = (b'!'..=b'~') // 94 printable ASCII chars
        .filter(|&b| b != b'"' && b != b'\\')
        .map(|b| b as char)
        .cycle()
        .take(95)
        .collect();
    let element = format!(r#""{}""#, value); // ~97 bytes each
    let elements_needed = target_bytes / (element.len() + 1) + 1;

    let mut out = String::with_capacity(target_bytes + 64);
    out.push('[');
    for i in 0..elements_needed {
        if i > 0 {
            out.push(',');
        }
        out.push_str(&element);
    }
    out.push(']');
    out
}

/// ~10 MiB JSON object with string keys and string values.
///
/// Keys are "key00000" … "key99999"; values are 85-char ASCII strings.
fn gen_string_object(target_bytes: usize) -> String {
    let value: String = (b'!'..=b'~')
        .filter(|&b| b != b'"' && b != b'\\')
        .map(|b| b as char)
        .cycle()
        .take(85)
        .collect();
    // "keyNNNNN":"<85 chars>"  ≈ 102 bytes per member
    let members_needed = target_bytes / 102 + 1;

    let mut out = String::with_capacity(target_bytes + 64);
    out.push('{');
    for i in 0..members_needed {
        if i > 0 {
            out.push(',');
        }
        out.push_str(&format!(r#""key{:05}":"{}""#, i % 100_000, value));
    }
    out.push('}');
    out
}

/// ~10 MiB nested mixed JSON.
///
/// Top-level array of objects; each object contains:
///   "id": <number>, "name": <string>, "active": <bool>,
///   "score": <null|number>, "tags": [<string>, <string>],
///   "meta": { "x": <number>, "y": <number> }
fn gen_mixed(target_bytes: usize) -> String {
    let tag_a = "alpha";
    let tag_b = "beta";
    // One record template (printed below) is ~130 bytes.
    let record_size = 130usize;
    let records_needed = target_bytes / record_size + 1;

    let mut out = String::with_capacity(target_bytes + 64);
    out.push('[');
    for i in 0..records_needed {
        if i > 0 {
            out.push(',');
        }
        let active = if i % 2 == 0 { "true" } else { "false" };
        let score = if i % 3 == 0 {
            "null".to_string()
        } else {
            format!("{}", i / 2)
        };
        out.push_str(&format!(
            r#"{{"id":{i},"name":"item{i}","active":{active},"score":{score},"tags":["{tag_a}","{tag_b}"],"meta":{{"x":{x},"y":{y}}}}}"#,
            i = i,
            active = active,
            score = score,
            tag_a = tag_a,
            tag_b = tag_b,
            x = i % 1000,
            y = (i * 7) % 1000,
        ));
    }
    out.push(']');
    out
}

// ---------------------------------------------------------------------------
// Benchmarks
// ---------------------------------------------------------------------------

const TARGET: usize = 10 * 1024 * 1024; // 10 MiB

fn bench_string_array(c: &mut Criterion) {
    let data = gen_string_array(TARGET);
    let mut group = c.benchmark_group("string_array");
    group.throughput(Throughput::Bytes(data.len() as u64));
    #[cfg(target_arch = "x86_64")]
    group.bench_function("asmjson/sax", |b| {
        b.iter(|| {
            let total = unsafe { parse_with_zmm(&data, LenSumWriter::new()) }.unwrap();
            std::hint::black_box(total)
        });
    });
    #[cfg(target_arch = "x86_64")]
    group.bench_function("asmjson/dom", |b| {
        b.iter(|| {
            let tape = unsafe { parse_to_dom_zmm(&data, None) }.unwrap();
            std::hint::black_box(dom_sum_lens(&tape))
        });
    });
    group.bench_function("asmjson/u64", |b| {
        b.iter(|| {
            let tape = parse_to_dom(&data, None).unwrap();
            std::hint::black_box(dom_sum_lens(&tape))
        });
    });
    group.bench_function("simd-json", |b| {
        b.iter(|| {
            let mut bytes = data.as_bytes().to_vec();
            let v = simd_json::to_borrowed_value(&mut bytes).unwrap();
            std::hint::black_box(simd_sum_lens(&v))
        });
    });
    group.bench_function("sonic-rs", |b| {
        b.iter(|| {
            let v = sonic_rs::from_str::<sonic_rs::Value>(&data).unwrap();
            std::hint::black_box(sonic_sum_lens(&v))
        });
    });
    group.bench_function("serde_json", |b| {
        b.iter(|| {
            let v = serde_json::from_str::<serde_json::Value>(&data).unwrap();
            std::hint::black_box(serde_sum_lens(&v))
        });
    });
    group.finish();
}

fn bench_string_object(c: &mut Criterion) {
    let data = gen_string_object(TARGET);
    let mut group = c.benchmark_group("string_object");
    group.throughput(Throughput::Bytes(data.len() as u64));
    #[cfg(target_arch = "x86_64")]
    group.bench_function("asmjson/sax", |b| {
        b.iter(|| {
            let total = unsafe { parse_with_zmm(&data, LenSumWriter::new()) }.unwrap();
            std::hint::black_box(total)
        });
    });
    #[cfg(target_arch = "x86_64")]
    group.bench_function("asmjson/dom", |b| {
        b.iter(|| {
            let tape = unsafe { parse_to_dom_zmm(&data, None) }.unwrap();
            std::hint::black_box(dom_sum_lens(&tape))
        });
    });
    group.bench_function("asmjson/u64", |b| {
        b.iter(|| {
            let tape = parse_to_dom(&data, None).unwrap();
            std::hint::black_box(dom_sum_lens(&tape))
        });
    });
    group.bench_function("simd-json", |b| {
        b.iter(|| {
            let mut bytes = data.as_bytes().to_vec();
            let v = simd_json::to_borrowed_value(&mut bytes).unwrap();
            std::hint::black_box(simd_sum_lens(&v))
        });
    });
    group.bench_function("sonic-rs", |b| {
        b.iter(|| {
            let v = sonic_rs::from_str::<sonic_rs::Value>(&data).unwrap();
            std::hint::black_box(sonic_sum_lens(&v))
        });
    });
    group.bench_function("serde_json", |b| {
        b.iter(|| {
            let v = serde_json::from_str::<serde_json::Value>(&data).unwrap();
            std::hint::black_box(serde_sum_lens(&v))
        });
    });
    group.finish();
}

fn bench_mixed(c: &mut Criterion) {
    let data = gen_mixed(TARGET);
    let mut group = c.benchmark_group("mixed");
    group.throughput(Throughput::Bytes(data.len() as u64));
    #[cfg(target_arch = "x86_64")]
    group.bench_function("asmjson/sax", |b| {
        b.iter(|| {
            let total = unsafe { parse_with_zmm(&data, LenSumWriter::new()) }.unwrap();
            std::hint::black_box(total)
        });
    });
    #[cfg(target_arch = "x86_64")]
    group.bench_function("asmjson/dom", |b| {
        b.iter(|| {
            let tape = unsafe { parse_to_dom_zmm(&data, None) }.unwrap();
            std::hint::black_box(dom_sum_lens(&tape))
        });
    });
    group.bench_function("asmjson/u64", |b| {
        b.iter(|| {
            let tape = parse_to_dom(&data, None).unwrap();
            std::hint::black_box(dom_sum_lens(&tape))
        });
    });
    group.bench_function("simd-json", |b| {
        b.iter(|| {
            let mut bytes = data.as_bytes().to_vec();
            let v = simd_json::to_borrowed_value(&mut bytes).unwrap();
            std::hint::black_box(simd_sum_lens(&v))
        });
    });
    group.bench_function("sonic-rs", |b| {
        b.iter(|| {
            let v = sonic_rs::from_str::<sonic_rs::Value>(&data).unwrap();
            std::hint::black_box(sonic_sum_lens(&v))
        });
    });
    group.bench_function("serde_json", |b| {
        b.iter(|| {
            let v = serde_json::from_str::<serde_json::Value>(&data).unwrap();
            std::hint::black_box(serde_sum_lens(&v))
        });
    });
    group.finish();
}

criterion_group!(
    benches,
    bench_string_array,
    bench_string_object,
    bench_mixed
);
criterion_main!(benches);