Struson version
0.7.2
Description
I want to read very large arrays of numbers into memory describing a graph (I'm trying to read the v8 heap snapshot format). Heap snapshots look like this:
{
"snapshot": {
"meta": {
...
}
...
},
// Large arrays with millions of numbers
"nodes": [9,1,1,0,10,0,0, 2,1,79,12,1,0,0, /* ... */],
"edges": [9,1,1,0,10,0,0, 2,1,79,12,1,0,0, /* ... */],
"locations": [9,1,1,0,10,0,0, 2,1,79,12,1,0,0, /* ... */],
...
}
Since snapshots are on the order of ~GBs, I thought a streaming parser would be more appropriate than serde, so I found and tried struson. If nothing else, since the expected sizes of the largest arrays are in the JSON file itself, using struson I could at least preallocate the Vecs to the correct size, so I could avoid the repeated resize+copy that a naive growing Vec allocation in serde_json would need to do.
But struson turns out not to be usable for this use case, because its number array parsing is almost twice as slow as serde_json, even accounting for the vec resizing that I'm able to avoid.
Expected behavior
Parsing a large array of numbers in struson to be competitive with serde_json (if not faster because of preallocation).
Actual behavior
The struson performance is a lot worse.
Reproduction steps
Here is a repro (it doesn't do the Vec preallocation mentioned above, but I'm getting ~the same numbers on this repo as on the real heap snapshot loading)
Use the following command to produce a file with a good amount of numbers in it:
python3 -c 'import json, random; json.dump(dict(numbers=[random.randint(0, 100000) for _ in range(2_000_000)]), open("numbers.json", "w"))'
Then run the following program:
use std::{error::Error, fs::File, io::BufReader, path::Path, time::Instant};
use serde::Deserialize;
use struson::{
json_path,
reader::{JsonReader, JsonStreamReader},
};
fn main() -> Result<(), Box<dyn Error>> {
{
let _t = start_timer("serde");
println!("{}", read_using_serde(Path::new("numbers.json"))?);
}
{
let _t = start_timer("struson");
println!("{}", read_using_struson(Path::new("numbers.json"))?);
}
Ok(())
}
//////////////////////////////////////////////////////////////////////
// SERDE
#[derive(Debug, Deserialize)]
pub struct NumbersFile {
pub numbers: Vec<u32>,
}
fn read_using_serde(path: &Path) -> Result<usize, Box<dyn Error>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let snapshot: NumbersFile = serde_json::from_reader(reader)?;
Ok(snapshot.numbers.len())
}
//////////////////////////////////////////////////////////////////////
// STRUSON
fn read_using_struson(path: &Path) -> Result<usize, Box<dyn Error>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut r = JsonStreamReader::new(reader);
// Accumulate into a Vec just like Serde would do
let mut numbers = Vec::<u32>::new();
r.seek_to(&json_path!["numbers"])?;
r.begin_array()?;
while r.has_next()? {
numbers.push(r.next_number()??);
}
r.end_array()?;
Ok(numbers.len())
}
//////////////////////////////////////////////////////////////////////
// TIMER
struct Timer {
name: String,
start: Instant,
}
fn start_timer(name: &str) -> Timer {
Timer {
name: name.to_string(),
start: Instant::now(),
}
}
impl Drop for Timer {
fn drop(&mut self) {
let duration = Instant::now() - self.start;
println!("{}: {:?}", self.name, duration);
}
}
Results:
$ cargo run
2000000
serde: 479.202834ms
2000000
struson: 823.245584ms
Release mode doesn't make a lot of difference:
$ cargo run --release
2000000
serde: 60.293ms
2000000
struson: 119.979833ms
Here's a flame graph I took of this:
A lot of time seems to be spent in the has_next(). Even if I take that out, and turn it into an optimistic read-and-recover:
r.seek_to(&json_path!["numbers"])?;
r.begin_array()?;
loop {
let next = r.next_number::<u32>();
if matches!(
next,
Err(ReaderError::UnexpectedStructure {
kind: struson::reader::UnexpectedStructureKind::FewerElementsThanExpected,
..
})
) {
break;
}
numbers.push(next??);
}
r.end_array()?;
It doesn't seem to make that much difference:
$ cargo run --release
2000000
serde: 56.756792ms
2000000
struson: 111.093458ms
(And what I highlighted only accounts for read_number_bytes, there is next_number_as_str and from_ascii_radix in the trace as well)
Struson version
0.7.2
Description
I want to read very large arrays of numbers into memory describing a graph (I'm trying to read the v8 heap snapshot format). Heap snapshots look like this:
Since snapshots are on the order of ~GBs, I thought a streaming parser would be more appropriate than
serde, so I found and triedstruson. If nothing else, since the expected sizes of the largest arrays are in the JSON file itself, usingstrusonI could at least preallocate theVecs to the correct size, so I could avoid the repeated resize+copy that a naive growing Vec allocation inserde_jsonwould need to do.But struson turns out not to be usable for this use case, because its number array parsing is almost twice as slow as
serde_json, even accounting for the vec resizing that I'm able to avoid.Expected behavior
Parsing a large array of numbers in
strusonto be competitive withserde_json(if not faster because of preallocation).Actual behavior
The
strusonperformance is a lot worse.Reproduction steps
Here is a repro (it doesn't do the Vec preallocation mentioned above, but I'm getting ~the same numbers on this repo as on the real heap snapshot loading)
Use the following command to produce a file with a good amount of numbers in it:
python3 -c 'import json, random; json.dump(dict(numbers=[random.randint(0, 100000) for _ in range(2_000_000)]), open("numbers.json", "w"))'Then run the following program:
Results:
Release mode doesn't make a lot of difference:
Here's a flame graph I took of this:
A lot of time seems to be spent in the
has_next(). Even if I take that out, and turn it into an optimistic read-and-recover:It doesn't seem to make that much difference:
(And what I highlighted only accounts for
read_number_bytes, there isnext_number_as_strandfrom_ascii_radixin the trace as well)