LEPTRIS

Benchmarks · XML

XML parsing and DOM operations

This page collects every measured XML race for the leptris ecosystem, with one section per language. Each section presents the standing matrix followed by the complete operation tables and their provenance, and the page closes with the consolidated measured-deficits table.

XML parse / DOM — competitorC / C++RubyPython
elementtree0.34×23.24×
libxml20.51×19.08×
lxml1.33×+
minidom12.78×+
moxml0.69×0.69×
nokogiri1.33×+
ox0.50×6.56×
pugixml0.20×3.36×

A green cell with a single value means every measured operation in that race is ahead, and the value shown is the smallest margin. A neutral cell shows the range from the weakest to the strongest operation; hovering lists the operations that remain behind. All rows, including deficits, appear in the tables below.

C and C++

libleptris versus libxml2 and pugixml

libxml2 is the de facto standard C parser and is the engine beneath lxml and Nokogiri. pugixml is a C++ library optimized for zero-copy DOM construction. The comparisons in this section run in a single harness on identical fixtures with identical iteration protocols, so every ratio is a same-binary baseline.

DOM parsing of a 2 MB text-heavy document, measured in C 3.4× vs pugixml

Minimum of 20 iterations; Release builds with identical flags on the same machine

libleptris 191 µs
pugixml 642 µs

Parse, SAX, serialization and mutation

operationleptris sidecompetitor (same run)standing
DOM parse — text-heavy 2 MB191 µs642 µs3.36×
DOM parse — text-heavy 2 MB191 µs966 µs5.06×
DOM parse — attr-heavy (5k attrs, 559 KB)2070 µs417 µs0.20×

the parse wall on this shape: single-pass at a compiler-global optimum is ~5x behind pugixml's zero-copy attr handling; no known lever

DOM parse — attr-heavy (5k attrs, 559 KB)2070 µs7985 µs3.86×
DOM parse — pretty-printed 1 MB2449 µs1217 µs0.50×

apples-to-apples caveat: leptris keeps whitespace-only text nodes (libxml2/Nokogiri semantics) while pugixml discards them; LEPTRIS_PARSE_DROP_WS_TEXT closes most of this gap on pretty shapes

DOM parse — pretty-printed 1 MB2449 µs23024 µs9.40×
DOM parse — catalog (5000 items, 367 KB)504 µs331 µs0.66×

mixed-content shapes carry the index bookkeeping pugixml skips

DOM parse — catalog (5000 items, 367 KB)504 µs3558 µs7.06×
SAX parse — text-heavy 2 MB905 µs859 µs0.95×
SAX parse — attr-heavy (5k attrs)2996 µs2311 µs0.77×
SAX parse — pretty-printed 1 MB4173 µs5757 µs1.38×
SAX parse — catalog (5000 items)1189 µs1136 µs0.95×
Serialize — attr-heavy 559 KB492 µs621 µs1.26×
Serialize — text-heavy 2 MB1080 µs657 µs0.61×

pugixml's zero-copy text spans win the pure-text shape; the 32-byte split-stream attr design carries its cost here

Append 10,000 children377 µs119 µs0.32×

doc-level index bookkeeping — the price of the O(1) duplicate-rejecting index at scale

Append 10,000 children377 µs544 µs1.44×
Set 2,000 attributes (duplicate-rejecting)480 µs198 µs0.41×

per-attr hash insert on tiny attrs; pugixml appends raw

Set 2,000 attributes (duplicate-rejecting)480 µs9156 µs19.08×

fresh runlibleptris 1.9.143-dev (main eea85cf)·min of 20 (Release + LTO)·Apple M1 Max, macOS 14.1.1, arm64··bench_matrix — same fixtures, same iteration protocol, all three libraries

DOM read operations

operationleptris sidelibxml2 2.9.13standing
DOM parse (~10 KB medium doc)7.43 µs42.38 µs5.70×
Tree traversal (full walk)2.19 µs1.11 µs0.51×

libxml2's next/prev pointers are bare struct hops; the arena layout pays a small toll per step

Attribute access (100×)2.92 µs3.24 µs1.11×
Text extraction (100×)1.13 µs7.82 µs6.92×
SAX parse — small (~1 KB)3.37 µs6.75 µs2.00×
SAX parse — medium (~5 KB)14.38 µs16 µs1.11×

fresh runlibleptris 1.9.143-dev (main eea85cf)·mean of 1000 (Release + LTO)·Apple M1 Max, macOS 14.1.1, arm64··DOM / SAX / XPath lane benches (bench_dom_*, bench_sax_*, bench_xpath_*)

To reproduce: configure withcmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DLEPTRIS_BUILD_BENCHMARKS=ONand run the binaries under build/benchmarks/. The harness is committed to the repository atbenchmarks.

Ruby

The leptris gem versus Nokogiri, Ox and moxml

The gem provides an API compatible with Nokogiri on top of libleptris. The operation matrix in this section was measured with the committed harness on identical fixtures. The remaining comparisons, against Ox and moxml, are carried from the performance ledger in the gem repository, and each of those rows is marked as an artifact with its provenance.

Parsing and serializing a 12 KB document through Ruby 9.2× / 3.1× vs Nokogiri

Identical fixtures and iteration counts from the committed matrix harness

leptris gem 1.9.121.020.4 µs
Nokogiri 1.19.4187.2 µs

Operation matrix versus Nokogiri

operationleptris sidenokogiri 1.19.4standing
Parse small (431 B)8.74 µs11.61 µs1.33×
Parse medium (12 KB)20.38 µs187.2 µs9.19×
Tree traversal (root.traverse)268.81 µs468.38 µs1.74×

formerly the one loss vs Nokogiri — fell to the batch-accessor sprint

Serialize (Document#to_xml)25.61 µs79.53 µs3.11×

fresh runleptris 1.9.121.0 (gem, lockstep with libleptris 1.9.121)·mean per iteration (µs)·Apple M1 Max, macOS 14.1.1, arm64; Ruby 3.4.8··leptris_vs_nokogiri.rb — same fixtures, same iteration counts

Tree traversal was the last operation to lose against Nokogiri in earlier releases and is now 1.74× ahead, following the batch-accessor work recorded in the gem changelog.

Whole-field comparison on a real-world document

operationleptris sidethe fieldstanding
Real-world document, 4.38 MB — parse + serialize (10,000 records, pretty-printed, encoding-declared)18 ms123 ms6.83×

carried from the serialbench full-field artifact behind the September site refresh; re-run when serialbench gains a local environment config

Real-world document, 4.38 MB — parse + serialize (10,000 records, pretty-printed, encoding-declared)18 ms118 ms6.56×

carried from the serialbench full-field artifact behind the September site refresh; re-run when serialbench gains a local environment config

artifactleptris gem 1.9.x line·per-iteration, same process·not recorded in the artifact··serialbench full-field run — artifact carried from the September 2026 site refresh (not re-run here)

The 4.38 MB real-world document comparison races Ox, Nokogiri, Oga and REXML in a single process. The serialbench harness is maintained in its own repository, where the leptris adapter is published.

Binding-internal improvement measurements

The following table is not a cross-library comparison. It measures the gem against earlier releases of itself on the same machine and with the same loops, using the committed read-path harness, and it is provided to show the effect of the memoization work in the 1.9 release line.

Loop over a 300-element read-only documentgem 1.9.2gem 1.9.17Result
Namespace inspection0.62 s0.017 s36× faster
Attribute reads0.63 s0.048 s13× faster
Text content reads0.19 s0.035 s5× faster

To reproduce: runbundle exec ruby -Ilib benchmark/leptris_vs_nokogiri.rbin the gem repository. The benchmark guide isin the repository.

Python

The leptris package versus lxml and the standard library

lxml is a C extension over the same libxml2 library that Nokogiri wraps, and ElementTree ships with Python with C acceleration. The leptris binding uses an abi3 C accelerator that is bound to libleptris through a fixed positional protocol. Wheels are published for macOS and manylinux on both architectures, and for Windows.

Parsing a 12 KB document through Python 4.6× vs lxml

Mean per iteration after warm-up, with garbage collection quieted during the timed loop

leptris 1.9.121.022.6 µs
lxml 6.0.2104.9 µs
ElementTree stdlib158.3 µs

Operation matrix against the full field

operationleptris sidelxml / stdlibstanding
Parse small (431 B)4.25 µs5.64 µs1.33×
Parse medium (12 KB)22.61 µs104.85 µs4.64×
Traversal (401 elements)9.86 µs19.35 µs1.96×
Traversal (401 elements)9.86 µs3.4 µs0.34×

ElementTree's iter() is a bare generator over a lean list — the stdlib's one crown; everything else it does is 2-23x behind

Serialize24.31 µs56.52 µs2.32×
Serialize24.31 µs565.07 µs23.24×
Serialize24.31 µs561.79 µs23.10×
Parse small (431 B)4.25 µs7.93 µs1.87×
Parse medium (12 KB)22.61 µs158.25 µs7.00×
Parse small (431 B)4.25 µs54.32 µs12.78×

fresh runleptris 1.9.121.0 (binding) on libleptris 1.9.143-dev·mean per iteration·Apple M1 Max, macOS 14.1.1, arm64; Python 3.10.5··benchmarks/matrix.py — same fixtures as the Ruby matrix

The binding is ahead of lxml on every measured operation. The only standard-library comparison it loses is traversal against ElementTree, whose iterator is a bare generator over a compact internal list. That deficit is listed in the measured-deficits table below and is tracked in the binding repository.

To reproduce: runLEPTRIS_LIB_PATH=…/libleptris.dylib python3 benchmarks/matrix.pyin the Python binding repository, which uses the same fixtures and operations as the Ruby matrix.

XML

Measured deficits

This table lists every XML operation in the result data that remains behind its competitor, ordered from the weakest ratio. Each row carries the harness it was measured with, and none of these rows is omitted from any other table on this site.

operationleptris sideversusstanding
DOM parse — attr-heavy (5k attrs, 559 KB)2070 µs417 µs0.20×

the parse wall on this shape: single-pass at a compiler-global optimum is ~5x behind pugixml's zero-copy attr handling; no known lever

Append 10,000 children377 µs119 µs0.32×

doc-level index bookkeeping — the price of the O(1) duplicate-rejecting index at scale

Traversal (401 elements)9.86 µs3.4 µs0.34×

ElementTree's iter() is a bare generator over a lean list — the stdlib's one crown; everything else it does is 2-23x behind

Set 2,000 attributes (duplicate-rejecting)480 µs198 µs0.41×

per-attr hash insert on tiny attrs; pugixml appends raw

DOM parse — pretty-printed 1 MB2449 µs1217 µs0.50×

apples-to-apples caveat: leptris keeps whitespace-only text nodes (libxml2/Nokogiri semantics) while pugixml discards them; LEPTRIS_PARSE_DROP_WS_TEXT closes most of this gap on pretty shapes

Native-loop traversalox 2.14.x0.50×

Ox's C walk holds the crown across the whole Ruby field — the one place it still wins anything; ~2x per the ledger

Tree traversal (full walk)2.19 µs1.11 µs0.51×

libxml2's next/prev pointers are bare struct hops; the arena layout pays a small toll per step

Serialize — text-heavy 2 MB1080 µs657 µs0.61×

pugixml's zero-copy text spans win the pure-text shape; the 32-byte split-stream attr design carries its cost here

DOM parse — catalog (5000 items, 367 KB)504 µs331 µs0.66×

mixed-content shapes carry the index bookkeeping pugixml skips

Expanded-name reads, NS-heavy loopmoxml 0.5.310.69×

measured off the C profile: the C resolver is ~20 ns/read either way — the cost is the adapter's per-read FFI fan-out. The batched-call lever (leptris_element_expanded_name, v1.9.144) shipped as an adapter primitive but was 4-way benchmarked for the gem's read path and does not pay there: the batch's buffer+tuple allocation outweighs the saved crossing. The row stays open on the adapter seam

SAX parse — attr-heavy (5k attrs)2996 µs2311 µs0.77×