This report presents benchmark results for Xpact-core, a pure-Eiffel XML parser porting the behaviour of the widely-used C library eXpat (libexpat). It accompanies the paper Finding a Billion-User Project for Eiffel: How DbC Catches Security Flaws Rust Misses, where the motivation and architecture of Xpact-core are described in detail.
Making it fast was the easy part
Getting Xpact to run quickly turned out to be relatively straightforward. The hard part was making it correct — matching eXpat's output exactly across every edge case: predefined entity resolution, partial-token buffer boundaries, CDATA section handling, namespace attribute semantics, and character-reference encoding. A mismatch in any one of these produces a different CRC-32 value from the two parsers, which is immediately detectable.
A dual-purpose correctness and benchmarking tool
To verify correctness and measure performance in the same run, I had Claude write a C program — xml_crc_32.c — that serves two purposes:
- It computes a CRC-32/ISO-HDLC hash over one of five parse-event data types (selectable on the command line), producing a single value that can be compared directly against Xpact's output.
- It counts complete parse passes in a fixed time window, giving a throughput figure for benchmarking.
The five data types are text content, CDATA sections, comments, start-tag names, and attribute values. The Eiffel counterpart is CRC_32_GENERATOR. When both programs produce identical CRC-32 values for a given file and data type, the parsers are in agreement for that combination.
Earlier in the project I used a simpler program — xml_tag_counter.c — which generates a table of tag-name occurrence frequencies. Its Eiffel counterpart is TAG_COUNTER. This confirmed basic parse-tree correctness but cannot detect errors in text content or attribute values.
Benchmark sessions are driven by two scripts — benchmark_Xpact_crc_32.sh and benchmark_Xpact_tag_count.sh — which run both parsers against each test file and append results to log files, allowing performance regressions to be tracked over time.
Where things stand
Overall I am happy with the result. I lost some performance during the correctness phase of the project, but not too much. I am hopefully approaching the Pareto proportion, though more testing is needed. The next goal is a mass-correctness testing program that exercises thousands of different files — as it turns out, a typical Linux install already has thousands of suitable test subjects on disk.
One robustness milestone worth noting: Xpact-core passes all five of the libexpat "hack attack" test files — aaaaaa_attr.xml, aaaaaa_cdata.xml, aaaaaa_comment.xml, aaaaaa_tag.xml, and aaaaaa_text.xml. Each is a ~10 MB document consisting of a single pathologically large token: a giant attribute value, CDATA block, comment, tag name, or text run with no newlines. They are designed to exercise exactly the buffer-shift and partial-token handling paths that are most likely to fail silently under adversarial input.
| OS | Linux Mint 22.2 (Ubuntu 24.04 Noble base) |
| PC | ThinkCentre M90a Pro Gen 6 · Intel Core Ultra 9 285 |
| EiffelStudio | 25.15, void-safe compilation |
| libexpat | 2.6.1 |
| GCC | 13.3.0 |
| Benchmark | eXpat passes | Xpact passes | Speed ratio |
|---|---|---|---|
| attribute | 28,026 | 38,205 | ×1.36 |
| cdata | 29,907 | 43,463 | ×1.45 |
| comment | 29,928 | 43,740 | ×1.46 |
| tag | 29,502 | 41,533 | ×1.41 |
| text | 29,415 | 40,629 | ×1.38 |
| tag_count | 29,295 | 38,767 | ×1.32 |
| Benchmark | eXpat passes | Xpact passes | Speed ratio |
|---|---|---|---|
| attribute | 1,294 | 1,344 | ×1.04 |
| cdata | 1,313 | 1,515 | ×1.15 |
| comment | 1,302 | 1,512 | ×1.16 |
| tag | 1,277 | 1,449 | ×1.13 |
| text | 1,151 | 1,248 | ×1.08 |
| tag_count | 1,279 | 1,355 | ×1.06 |
| Benchmark | eXpat passes | Xpact passes | Speed ratio |
|---|---|---|---|
| attribute | 242 | 281 | ×1.16 |
| cdata | 245 | 297 | ×1.21 |
| comment | 245 | 293 | ×1.20 |
| tag | 237 | 281 | ×1.19 |
| text | 210 | 215 | ×1.02 |
| tag_count | 244 | 265 | ×1.09 |
storeAttributes() duplicate-attribute check loop scales with attribute count, increasing its per-element cost significantly.| Benchmark | eXpat passes | Xpact passes | Speed ratio |
|---|---|---|---|
| attribute | 17 | 25 | ×1.47 |
| cdata | 18 | 30 | ×1.67 |
| comment | 18 | 30 | ×1.67 |
| tag | 18 | 30 | ×1.67 |
| text | 18 | 29 | ×1.61 |
| tag_count | 17 | 30 | ×1.76 |
storeAttributes() and prefix-expansion the dominant cost for eXpat. Xpact's simpler attribute model avoids this overhead entirely.| Benchmark | eXpat passes | Xpact passes | Speed ratio |
|---|---|---|---|
| attribute | 15 | 21 | ×1.40 |
| cdata | 15 | 25 | ×1.67 |
| comment | 15 | 25 | ×1.67 |
| tag | 15 | 26 | ×1.73 |
| text | 16 | 25 | ×1.56 |
| tag_count | 14 | 24 | ×1.71 |
Speed Ratio — All Benchmarks
Xpact passes ÷ eXpat passes · last run per log · all values ≥ 1.0 (Xpact leads in every case)
| File | attribute | cdata | comment | tag | text | tag_count | avg |
|---|---|---|---|---|---|---|---|
| vision2.ecf | ×1.36 | ×1.45 | ×1.46 | ×1.41 | ×1.38 | ×1.32 | ×1.40 |
| Legislation.xml | ×1.04 | ×1.15 | ×1.16 | ×1.13 | ×1.08 | ×1.06 | ×1.10 |
| nes96.xml | ×1.16 | ×1.21 | ×1.20 | ×1.19 | ×1.02 | ×1.09 | ×1.15 |
| recset.xml | ×1.47 | ×1.67 | ×1.67 | ×1.67 | ×1.61 | ×1.76 | ×1.64 |
| ns_att_test.xml | ×1.40 | ×1.67 | ×1.67 | ×1.73 | ×1.56 | ×1.71 | ×1.62 |
| avg per type | ×1.29 | ×1.43 | ×1.43 | ×1.43 | ×1.33 | ×1.39 | ×1.38 |
storeAttributes() runs a duplicate-attribute check that scales linearly with attribute count — a cost Xpact avoids at the scanner level.