0.42.0 is about 30% slower than 0.41.0 on documents with many small events
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- rust
- Domain
- backend, performance
Research direction
Start by examining the UTF-8 validation added in #963 and the reader paths used by read_event_into and read_event; the issue provides a standalone benchmark repro in Cargo.toml and src/main.rs. Run the benchmark against 0.41.0 and 0.42.0, then compare event-level validation with the reported whole-document check. Done means preserving correct UTF-8 validation while reducing the measured regression on many-small-event documents.
Written by the indexing model from the issue text.
Description
After upgrading from 0.41.0 to 0.42.0, reading a document made of many small events takes about 30% longer, with both read_event_into and read_event. The document in the repro below is shaped like a spreadsheet sheet: 200,000 rows, 2.4 million events, 19.9 MB.
We noticed it through calamine, which moved to 0.42 in tafia/calamine#711: reading a 200,000-row .xlsx sheet with worksheet_range takes about 12% longer with calamine 0.37.0 than with 0.36.1 (174 ms to 196 ms), and the added 21 ms matches what the parser alone adds on that sheet.
The per-event UTF-8 validation added in #963 looks like the cause. Adding a std::str::from_utf8 call on each event's bytes to 0.41.0 costs a little more than the upgrade does. Checking the whole document at once takes about 1 ms, so the cost seems to come from making one call per event, about 11 ns each, rather than from the checking itself.
| 19.9 MB, 2,400,005 events | 0.41.0 | 0.42.0 | Change |
|---|---|---|---|
read_event_into, BufReader<&[u8]> |
84.5 ms | 110.8 ms | +31% |
read_event, &[u8] |
57.5 ms | 75.5 ms | +31% |
0.41.0 read_event_into plus from_utf8 per event |
116.7 ms | +38% | |
from_utf8 of the whole document |
1.1 ms |
Medians of 15 interleaved runs; a second run agreed within about 1%. With the encoding feature, the changes are +25% and +33%. Windows 11, AMD EPYC 7763, rustc 1.99.0, release build.
Two ideas, in case they help:
Reader::from_strtakes input that is already valid UTF-8, but its events are still checked, because it reads through the&[u8]reader.- Checking larger spans at once, such as each chunk that
fill_bufreturns, might cost much less than one call per event.
Repro: Cargo.toml and src/main.rs
[package]
name = "qx-utf8-bench"
version = "0.1.0"
edition = "2021"
[dependencies]
qx41 = { package = "quick-xml", version = "=0.41.0" }
qx42 = { package = "quick-xml", version = "=0.42.0" }
[features]
encoding = ["qx41/encoding", "qx42/encoding"]
use std::fmt::Write;
use std::hint::black_box;
use std::io::BufReader;
use std::time::Instant;
/// A document shaped like a spreadsheet sheet: one row per record, two cells each.
fn document(rows: usize) -> String {
let mut xml = String::from(concat!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>"#
));
for r in 1..=rows {
write!(
xml,
r#"<row r="{r}" spans="1:2"><c r="A{r}" t="s"><v>{}</v></c><c r="B{r}"><v>{}</v></c></row>"#,
r % 1000,
r * 7
)
.unwrap();
}
xml.push_str("</sheetData></worksheet>");
xml
}
fn buffered_041(xml: &[u8]) -> usize {
let mut reader = qx41::Reader::from_reader(BufReader::new(xml));
let mut buf = Vec::new();
let mut events = 0;
loop {
match reader.read_event_into(&mut buf).unwrap() {
qx41::events::Event::Eof => return events,
_ => events += 1,
}
buf.clear();
}
}
fn buffered_042(xml: &[u8]) -> usize {
let mut reader = qx42::Reader::from_reader(BufReader::new(xml));
let mut buf = Vec::new();
let mut events = 0;
loop {
match reader.read_event_into(&mut buf).unwrap() {
qx42::events::Event::Eof => return events,
_ => events += 1,
}
buf.clear();
}
}
fn borrowed_041(xml: &[u8]) -> usize {
let mut reader = qx41::Reader::from_reader(xml);
let mut events = 0;
loop {
match reader.read_event().unwrap() {
qx41::events::Event::Eof => return events,
_ => events += 1,
}
}
}
fn borrowed_042(xml: &[u8]) -> usize {
let mut reader = qx42::Reader::from_reader(xml);
let mut events = 0;
loop {
match reader.read_event().unwrap() {
qx42::events::Event::Eof => return events,
_ => events += 1,
}
}
}
/// 0.41.0 plus a `from_utf8` check of each event's bytes.
fn buffered_041_checked(xml: &[u8]) -> usize {
use qx41::events::Event;
let mut reader = qx41::Reader::from_reader(BufReader::new(xml));
let mut buf = Vec::new();
let mut events = 0;
loop {
let event = reader.read_event_into(&mut buf).unwrap();
let bytes: &[u8] = match &event {
Event::Eof => return events,
Event::Start(e) | Event::Empty(e) => &**e,
Event::End(e) => &**e,
Event::Text(e) => &**e,
_ => &[],
};
events += black_box(std::str::from_utf8(bytes)).is_ok() as usize;
buf.clear();
}
}
fn whole_document(xml: &[u8]) -> usize {
std::str::from_utf8(xml).unwrap().len()
}
fn main() {
let xml = document(200_000);
let xml = xml.as_bytes();
let cases: [(&str, fn(&[u8]) -> usize); 6] = [
("0.41.0 read_event_into, BufReader<&[u8]>", buffered_041),
("0.42.0 read_event_into, BufReader<&[u8]>", buffered_042),
("0.41.0 read_event, &[u8]", borrowed_041),
("0.42.0 read_event, &[u8]", borrowed_042),
("0.41.0 read_event_into + from_utf8 per event", buffered_041_checked),
("from_utf8 of the whole document", whole_document),
];
let events = buffered_041(xml);
for (name, case) in &cases[1..5] {
assert_eq!(case(xml), events, "{name}");
}
println!("{:.1} MB, {events} events", xml.len() as f64 / 1e6);
let rounds = 15;
let mut times = vec![Vec::new(); cases.len()];
for round in 0..rounds {
for k in 0..cases.len() {
let i = (k + round) % cases.len();
let start = Instant::now();
black_box((cases[i].1)(black_box(xml)));
times[i].push(start.elapsed().as_secs_f64() * 1e3);
}
}
for (i, (name, _)) in cases.iter().enumerate() {
times[i].sort_by(f64::total_cmp);
println!("{name:46} {:7.1} ms", times[i][rounds / 2]);
}
}
- Dominant language
- Rust
- Stars
- 1.6k
- Forks
- 299
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 9
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from tafia/quick-xml
-
Parser accepts an empty element or attribute namePossibly taken @ychampion claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
DecodingReader::set_encoding panics when the XML declaration is longer than 64 bytesPossibly taken @emschwartz claimed this 4 days ago. Openbug encoding
Difficulty 4/5 3-5 days Newbie friendliness 30/100
tafia/quick-xml#1040 · 1 comment ·
Maintainers usually reply within 1 day
-
optimization question
Difficulty 4/5 3-5 days Newbie friendliness 52/100
Maintainers usually reply within 1 day
-
enhancement
Difficulty 4/5 1-2 days Newbie friendliness 53/100
tafia/quick-xml#1035 · 1 comment ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 25/100
Maintainers usually reply within 1 day
Similar issues
-
comp-formats
Difficulty 2/5 Half a day Newbie friendliness 64/100
ClickHouse/ClickHouse#131694 ·
Maintainers usually reply within 1 day
-
cli: `execCargoMetadata` decodes stdout per chunk, corrupting non-ASCII paths across pipe boundariesOpen
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Copperpour/Copperpour#500 ·
Maintainers usually reply within 1 day
-
state:triage-needed
Difficulty 1/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day