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:

  1. 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.
  2. 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.

Test environment
OSLinux Mint 22.2 (Ubuntu 24.04 Noble base)
PCThinkCentre M90a Pro Gen 6 · Intel Core Ultra 9 285
EiffelStudio25.15, void-safe compilation
libexpat2.6.1
GCC13.3.0
Metric
Complete parse passes in a fixed 2-second window — higher is faster
Speed ratio
Xpact passes ÷ eXpat passes · a value of ×1.50 means Xpact is 50% faster
vision2.ecf 13 KB
ISE Eiffel config attribute-driven no text content shallow nesting
Structure
Size13 KB
Open tags212
Attributes238
Avg attrs / tag1.1
CDATA sections0
Comments0
Entity refs0
The ISE Eiffel project configuration format — a compact, well-formed document. Its tiny size (~13 KB) means each parse completes in tens of microseconds, producing very high loop counts and exposing per-invocation dispatch overhead more clearly than throughput.
Benchmark eXpat passes Xpact passes Speed ratio
attribute28,02638,205×1.36
cdata29,90743,463×1.45
comment29,92843,740×1.46
tag29,50241,533×1.41
text29,41540,629×1.38
tag_count29,29538,767×1.32
legislative text mixed content named entity refs attribute-heavy
Structure
Size419 KB
Open tags7,628
Attributes13,669
Avg attrs / tag1.8
CDATA sections0
Comments0
Entity refs20
New Zealand legislation in XML form — a realistic mixed-content document with meaningful text between elements, moderate attribute density, and 20 named entity references requiring predefined entity resolution (&, <, etc.).
Benchmark eXpat passes Xpact passes Speed ratio
attribute1,2941,344×1.04
cdata1,3131,515×1.15
comment1,3021,512×1.16
tag1,2771,449×1.13
text1,1511,248×1.08
tag_count1,2791,355×1.06
nes96.xml 2.9 MB
survey data mixed content no namespaces text-heavy
Structure
Size2.9 MB
Open tags39,107
Attributes29,479
Avg attrs / tag0.75
CDATA sections0
Comments0
Entity refs39
US National Election Study 1996 survey data. Fewer attributes per element than Legislation.xml but richer text content — the data-in-text pattern means more time is spent in the data-character scanner, where raw byte throughput dominates over dispatch overhead.
Benchmark eXpat passes Xpact passes Speed ratio
attribute242281×1.16
cdata245297×1.21
comment245293×1.20
tag237281×1.19
text210215×1.02
tag_count244265×1.09
recset.xml 29 MB
record set flat structure attribute-dense partial namespaces
Structure
Size29 MB
Open tags50,069
Attributes1,388,668
Avg attrs / tag27.7
CDATA sections0
Comments0
Char refs316
Record-set data donated to libexpat by Karl Waclawek. Extremely attribute-dense (≈27.7 per tag), flat, mixed namespace/non-namespace. eXpat's storeAttributes() duplicate-attribute check loop scales with attribute count, increasing its per-element cost significantly.
Benchmark eXpat passes Xpact passes Speed ratio
attribute1725×1.47
cdata1830×1.67
comment1830×1.67
tag1830×1.67
text1829×1.61
tag_count1730×1.76
namespace stress test flat structure 28 prefixed attrs / element largest corpus
Structure
Size35 MB
Open tags50,002
Attributes1,388,340
Avg attrs / tag27.8
CDATA sections0
Comments0
Char refs316
Designed specifically to stress-test libexpat's namespace attribute handling. Every element carries ≈28 namespace-prefixed attributes, making 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
attribute1521×1.40
cdata1525×1.67
comment1525×1.67
tag1526×1.73
text1625×1.56
tag_count1424×1.71
Summary

Speed Ratio — All Benchmarks

Xpact passes ÷ eXpat passes · last run per log · all values ≥ 1.0 (Xpact leads in every case)

≥ ×1.50 — strong lead ×1.30–1.49 — clear lead ×1.10–1.29 — moderate lead < ×1.10 — marginal lead
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
Observations
Finding 01
Xpact leads on every single benchmark
Across all 5 files and 6 benchmark types (30 data points), Xpact-core is faster in every case. The overall average is ×1.38 — the pure-Eiffel parser completes 38% more parse cycles per second than the reference C library.
Finding 02
Attribute-heavy files show the largest gains
recset.xml (×1.64 avg) and ns_att_test.xml (×1.62 avg) are where the gap is widest. Both carry ~28 attributes per element. eXpat's storeAttributes() runs a duplicate-attribute check that scales linearly with attribute count — a cost Xpact avoids at the scanner level.
Finding 03
Text-content parsing is the tightest contest
The text benchmark is consistently the smallest gain: ×1.02 on nes96.xml (nearly at parity), ×1.08 on Legislation.xml. Text-heavy documents spend most cycles in the data-character scan loop where throughput is dominated by raw memory bandwidth — Eiffel's overhead is smallest here.
Finding 04
Small files reveal per-invocation overhead
vision2.ecf at 13 KB completes ~43,000 Xpact passes per second. Its ×1.40 avg shows Xpact has meaningfully lower per-parse startup and dispatch cost than eXpat — a difference obscured in large files where data movement dominates. Peak Xpact throughput across all runs is ~477 MB/s (ns_att_test, tag benchmark).