serde_json_borrow 0.8: Faster than SIMD?


serde_json_borrow 0.8

serde_json_borrow 0.8 is out! This release brings a major performance improvement. It is now the fastest JSON parser in all benchmarks, even outperforming simd_json.

What is it

serde_json_borrow builds on serde_json’s deserializer to produce a serde_json_borrow::Value<'ctx> that can borrow directly from the input buffer and reference string slices and other data directly from the input buffer.

It’s ideal for scenarios where JSON is used as a transient representation to extract some data or apply some transformation.

What Changed in 0.8

The main change and performance improvement comes from PR #32 (thanks @jszwec). Previously, serde’s default behavior would always create Cow::Owned for object keys even when borrowing was possible (see serde-rs/serde#1852). To address this, serde_json_borrow introduces a custom wrapper around Cow to allow borrowed keys.

Value vs OwnedValue

serde_json_borrow provides two main ways to work with parsed JSON:

  • Value<'ctx>: This variant borrows string slices and other data directly from the input buffer. It requires that the input &'ctx str outlives the parsed Value<'ctx>.

  • OwnedValue: This variant clones the serialized JSON &str and therefore does not depend on the input buffer’s lifetime.

cowkeys Feature Flag

The feature flag cowkeys uses Cow<str> instead of &str as keys in objects. This enables support for escaped strings. The feature is enabled by default, but can be disabled if you know your JSON data does not contain escaped strings.

Benchmark

The benchmark results show that serde_json_borrow is now the fastest choice, even outperforming simd_json in all test cases.

Benchmark Setup

The benchmarks were run on a machine with the following specifications:

  • CPU: AMD Ryzen 9 9800X3D
  • RAM: 32 GB
  • OS: 6.14.6-2-MANJARO
  • Disk: Samsung SSD 990 PRO
  • Rust Version: 1.87.0
  • Benchmarking Tool: binggan

The benchmarks were configured to trash the CPU cache and branch predictor (or at least try to 🙂) to ensure that the performance measurements are not influenced by caching effects. This uses the CacheTrasher and BPUTrasher plugins from the binggan crate:

let mut runner: BenchRunner = BenchRunner::new();
runner
    .add_plugin(CacheTrasher::default())
    .add_plugin(BPUTrasher::default());

You can run the benchmarks yourself with cargo bench on the https://github.com/serde_json_borrow/tree/blog_post_benchmark branch in the serde_json_borrow github repo.

The benchmarks below are the worst case configuration for serde_json_borrow:

  • Only OwnedValue is tested (but the data is already String).
  • The cowkeys feature is enabled, which allows for escaped keys in JSON objects.

Accessing the Parsed JSON

One of the main differences between serde_json_borrow and serde_json is that serde_json_borrow uses a Vec for objects instead of a BTreeMap. This improves deserialization performance, but reduces read-access performance for very large objects. The benchmark reads several keys 10 times from the parsed JSON object. Read access is dwarfed by the parsing performance in this benchmark.

For objects with very few keys, a scan of the Vec is faster than a binary search in a BTreeMap. The break-even probably between 10 and 30 keys.

Results

Dataset: simple_jsonAvg MB/sMedian MB/sRange Low MB/sRange High MB/s
serde_json309.41309.42297.48312.87
serde_json + access302.63303.51292.17306.87
serde_json_borrow::OwnedValue551.28550.57537.65561.81
serde_json_borrow::OwnedValue + access528.08528.61510.66542.12
serde_json_borrow v0.7::OwnedValue440.91440.59429.04449.27
simd_json_borrow291.32291.51285.61292.38
Dataset: hdfsAvg MB/sMedian MB/sRange Low MB/sRange High MB/s
serde_json684.07685.38672.70689.32
serde_json + access690.25687.36664.73700.37
serde_json_borrow::OwnedValue1138.481143.401047.451156.10
serde_json_borrow::OwnedValue + access1128.961121.381085.851163.26
serde_json_borrow v0.7::OwnedValue893.49895.36827.68911.69
simd_json_borrow688.15690.37659.29692.93
Dataset: hdfs_with_arrayAvg MB/sMedian MB/sRange Low MB/sRange High MB/s
serde_json460.97461.01453.62474.36
serde_json + access475.77476.88464.11485.78
serde_json_borrow::OwnedValue817.97819.37795.93834.68
serde_json_borrow::OwnedValue + access817.90818.61788.71833.57
serde_json_borrow v0.7::OwnedValue731.79731.43717.81741.30
simd_json_borrow539.51540.13533.17541.27
Dataset: wikiAvg MB/sMedian MB/sRange Low MB/sRange High MB/s
serde_json1354.501354.401337.201367.90
serde_json + access1397.201397.601385.001414.60
serde_json_borrow::OwnedValue1510.701514.301480.301537.00
serde_json_borrow::OwnedValue + access1558.601558.401536.701583.30
serde_json_borrow v0.7::OwnedValue1463.101463.601447.101481.30
simd_json_borrow1473.601473.701459.001483.20
Dataset: gh-archiveAvg MB/sMedian MB/sRange Low MB/sRange High MB/s
serde_json450.44450.08439.20456.40
serde_json + access447.61446.50442.51455.33
serde_json_borrow::OwnedValue1147.901143.911124.971170.94
serde_json_borrow::OwnedValue + access1154.051154.361129.161185.28
serde_json_borrow v0.7::OwnedValue690.87689.07682.23700.78
simd_json_borrow995.81996.01991.79999.33

Conclusion

Noice, isn’t it?