Benchmarks¶
Comprehensive performance comparison between all json2xml implementations.
Test Environment¶
Machine: Apple Silicon (arm64)
OS: macOS 26.5 (Darwin 25.5.0)
Python: 3.14.4
Date: May 27, 2026
CLI tools:
json2xml-goandjson2xml-zigfrom/Users/vinitkumar/.local/bin
Implementations Tested¶
Implementation |
Type |
Notes |
|---|---|---|
Python |
Library |
Pure Python |
Rust |
Library |
Native extension via PyO3, imported as |
Go |
CLI |
Standalone |
Zig |
CLI |
Standalone |
Test Data¶
Size |
Description |
Bytes |
|---|---|---|
Small |
Simple object |
47 |
Medium |
10 generated records with nested structures |
3,215 |
bigexample.json |
Real-world patent data |
2,018 |
Large |
100 generated records with nested structures |
32,206 |
Very Large |
1,000 generated records with nested structures |
323,131 |
Results¶
Performance Summary¶
Test Case |
Python |
Rust |
Go |
Zig |
|---|---|---|---|---|
Small (47B) |
3.19µs |
0.86µs |
6.05ms |
3.08ms |
Medium (3.2KB) |
214.83µs |
18.41µs |
5.85ms |
3.12ms |
bigexample (2KB) |
91.20µs |
7.32µs |
5.76ms |
3.08ms |
Large (32KB) |
2.07ms |
175.46µs |
5.89ms |
3.73ms |
Very Large (323KB) |
21.20ms |
1.48ms |
6.82ms |
7.82ms |
Speedup vs Pure Python¶
Test Case |
Rust |
Go |
Zig |
|---|---|---|---|
Small (47B) |
3.7x |
0.0x* |
0.0x* |
Medium (3.2KB) |
11.7x |
0.0x* |
0.1x* |
bigexample (2KB) |
12.5x |
0.0x* |
0.0x* |
Large (32KB) |
11.8x |
0.4x* |
0.6x* |
Very Large (323KB) |
14.4x |
3.1x |
2.7x |
CLI tools have process spawn overhead of about 3-6ms, which dominates for small inputs.
Security Hardening Benchmark¶
This August 12, 2026 comparison measures the public Json2xml(...).to_xml() path before and after conversion limits, lexical pretty printing, and compact output by default were added. It is kept as a dated record; the pretty-output figures were superseded on August 23, 2026, when indentation moved into the serializer and the lexical formatter was removed.
It compares pre-hardening commit 826439f with hardened master at 48dfd38 on Apple Silicon and macOS 26.6.1. The primary run used CPython 3.14.6.
The deterministic benchmark used small, 100-record, and 1,000-record nested payloads generated by benchmarks/benchmark_security_hardening.py. Record fields depend only on their zero-based index, so no pseudorandom seed or hidden input is required.
The committed harness contains the exact payloads, loop counts, worker order, timing code, and UTF-8 output checks. It creates detached temporary worktrees for both revisions and launches fresh workers in mirrored ABBA and BAAB orders. Two passes provide four workers per revision; 17 samples per worker yield 68 samples per revision and cell after five warmups per worker.
Every child process receives an argv list with shell parsing disabled. User-supplied revision text follows Git’s --end-of-options marker, and only the resulting full commit ID is passed to git worktree add; no shell escaping is required.
Workload |
Mode |
Pre-hardening |
Hardened master |
Change |
|---|---|---|---|---|
Small |
Default |
24.1µs |
6.3µs |
74.0% faster |
Small |
Compact |
4.4µs |
6.4µs |
44.9% slower |
Small |
Pretty |
24.3µs |
15.1µs |
38.0% faster |
100 records |
Default |
7.07ms |
1.80ms |
74.6% faster |
100 records |
Compact |
1.14ms |
1.83ms |
60.6% slower |
100 records |
Pretty |
6.87ms |
5.04ms |
26.7% faster |
1,000 records |
Default |
86.79ms |
17.44ms |
79.9% faster |
1,000 records |
Compact |
10.92ms |
17.40ms |
59.3% slower |
1,000 records |
Pretty |
85.03ms |
49.26ms |
42.1% faster |
CPython 3.15.0rc1 Follow-up¶
uv 0.12.3 downloaded and managed the exact cpython-3.15.0rc1-macos-aarch64-none build. The follow-up uses the same payloads, modes, warmups, sample counts, worker isolation, and mirrored ABBA/BAAB ordering.
uv 0.11.24 did not yet list rc1, so it was updated to 0.12.3. That release downloaded the exact 26.2 MiB Apple Silicon build and verified its version before measurement.
uv self update
uv python list 3.15 --all-versions
uv run --isolated --managed-python \
--python cpython-3.15.0rc1-macos-aarch64-none \
--with defusedxml \
--with 'urllib3>=2.7.0' \
python benchmarks/benchmark_security_hardening.py \
--before 826439f \
--after 48dfd38 \
--output-json /tmp/json2xml-security-benchmark-python315rc1.json
The console report includes medians, interquartile ranges, output types, UTF-8 byte counts, and SHA-256 values. The optional JSON file preserves all 1,224 raw timings: 9 cells × 2 revisions × 68 samples.
Each result is median [p25, p75]. The narrow hardened ranges contrast with the wider old DOM-pretty ranges on larger payloads.
Workload |
Mode |
Pre-hardening median [p25, p75] |
Hardened median [p25, p75] |
Change |
|---|---|---|---|---|
Small |
Default |
24.7µs [24.6, 25.0] |
6.2µs [6.2, 6.3] |
74.8% faster |
Small |
Compact |
4.4µs [4.4, 4.4] |
6.4µs [6.3, 6.4] |
44.0% slower |
Small |
Pretty |
24.7µs [24.5, 24.8] |
14.8µs [14.7, 14.9] |
40.0% faster |
100 records |
Default |
7.13ms [6.65, 8.50] |
1.75ms [1.74, 1.76] |
75.5% faster |
100 records |
Compact |
1.09ms [1.09, 1.11] |
1.75ms [1.74, 1.79] |
60.3% slower |
100 records |
Pretty |
6.85ms [6.61, 8.47] |
4.96ms [4.90, 5.03] |
27.5% faster |
1,000 records |
Default |
88.01ms [62.86, 100.53] |
17.24ms [17.17, 17.30] |
80.4% faster |
1,000 records |
Compact |
10.90ms [10.87, 10.98] |
17.21ms [17.15, 17.28] |
57.8% slower |
1,000 records |
Pretty |
88.83ms [63.04, 102.21] |
50.46ms [49.61, 51.15] |
43.2% faster |
Output Checks¶
Compact output stayed byte-for-byte identical across revisions. Default and pretty sizes differ because default now returns compact bytes and lexical formatting replaces minidom formatting.
Workload |
Compact bytes, both |
Default type/bytes, before → after |
Pretty type/bytes, before → after |
|---|---|---|---|
Small |
134 |
|
|
100 records |
57,049 |
|
|
1,000 records |
571,005 |
|
|
The compact-output SHA-256 values are 013c28cd992bb5caa9a3c357a492ab47ccf00ad479ad3010e7ec376754cb06a8 for small, 05d79278d394c70a816fce23aa60e2fd0059233abf94c863a26e17af4dcdb090 for 100 records, and 72de1be56cbc381c25606a3ca5c73b40d482ee08ef2d74f93d3b2798e531d760 for 1,000 records.
Default calls are roughly 4-5x faster because they now return compact serializer bytes. Explicit pretty output was 27-42% faster in this run because lexical indentation replaced DOM parsing; the later change below removed that formatter entirely.
Explicit compact output is 45-61% slower. Its bytes were identical and the serializer was unchanged, isolating the regression mainly to the new full-input resource-budget scan.
CPython 3.15.0rc1 reproduces the same tradeoff: default and pretty output improve substantially, while explicit compact conversion pays for the security scan.
PyPy 3.10.16 corroborated the tradeoff: default calls were 73-87% faster, pretty calls were 42-72% faster, and compact calls were 31-35% slower.
Generation-Time Pretty Printing Benchmark¶
This August 23, 2026 comparison supersedes the lexical-formatter results above for
pretty output. It measures the same public Json2xml(...).to_xml() path before and
after indentation moved into the serializer itself.
Previously pretty=True serialized compact XML and then re-tokenized that output to
insert indentation, a second full pass over generated markup. The serializer now
receives an indent unit and emits indentation as it writes, so the tokenizer is gone
and no XML is parsed at any point.
It compares 78ea9ea with 7b29472 on Apple Silicon and macOS 26.6.1 using
CPython 3.14.6, driven by the same benchmarks/benchmark_security_hardening.py
harness, payloads, worker order, and 68-samples-per-cell methodology described above.
Workload |
Mode |
Before |
After |
Change |
|---|---|---|---|---|
Small |
Default |
6.4us [6.4, 6.5] |
6.5us [6.5, 6.6] |
1.5% slower |
Small |
Compact |
6.5us [6.4, 6.5] |
6.6us [6.6, 6.7] |
2.3% slower |
Small |
Pretty |
15.2us [15.1, 15.3] |
8.2us [8.1, 8.2] |
46.2% faster |
100 records |
Default |
1.69ms [1.68, 1.70] |
1.68ms [1.68, 1.70] |
0.4% faster |
100 records |
Compact |
1.68ms [1.67, 1.69] |
1.69ms [1.68, 1.71] |
0.5% slower |
100 records |
Pretty |
4.86ms [4.81, 4.90] |
2.36ms [2.35, 2.37] |
51.4% faster |
1,000 records |
Default |
16.75ms [16.63, 16.88] |
16.55ms [16.44, 16.69] |
1.2% faster |
1,000 records |
Compact |
16.64ms [16.59, 16.73] |
16.76ms [16.57, 16.87] |
0.7% slower |
1,000 records |
Pretty |
49.40ms [48.70, 49.90] |
23.18ms [23.00, 23.37] |
53.1% faster |
Pretty conversion is roughly twice as fast, and the gain grows with payload size because the removed pass was proportional to output length. Compact and default calls move within measurement noise, which is the intended result: the compact writer keeps its previous hot path and treats the structural calls as plain writes.
Generation-Time Output Checks¶
All nine cells produced identical SHA-256 values before and after, pretty output
included, so the speedup is not paid for with changed output. Note that these payloads
contain no null values or empty containers; an empty element such as
<a type="null"></a> is the one case whose pretty layout did change, since the old
tokenizer split it across two lines.
Key Observations¶
Rust remains the best option for Python library calls. It avoids process overhead and is about 4-14x faster than the optimized pure Python path in this run.
Recent pure Python improvements substantially reduced conversion time. Medium and large inputs are roughly an order of magnitude faster than the April 2026 baseline, so relative Rust speedups are lower even though Rust is still fastest.
Go and Zig remain useful for native CLI workflows. They are slower for small and medium inputs because startup dominates, but both beat Python on the 323KB workload when full CLI process time is measured.
See docs/rust_memory_benchmark.rst for the June 2026 Rust peak-memory comparison between the previous extension-boundary copy and the Python bytes-writer implementation.
When to Use Each Implementation¶
Use Case |
Recommended |
Why |
|---|---|---|
Python library calls |
Rust |
4-14x faster, no process overhead |
Small files via CLI |
Zig |
Fastest startup among native CLIs in this run |
Large files via CLI |
Go or Zig |
Both are faster than Python at 323KB |
Batch processing |
Go or Rust |
Choose based on shell vs Python integration |
Pure Python required |
Python |
Always available |
Running the Benchmarks¶
Run benchmarks from a clean checkout with the project installed in an isolated environment.
uv venv
source .venv/bin/activate
uv pip install -e .
python benchmarks/benchmark_all.py
To reproduce the security-hardening public-API comparison on the active Python interpreter, run the committed harness. It removes its detached temporary worktrees without changing the current checkout.
python benchmarks/benchmark_security_hardening.py \
--before 826439f \
--after 48dfd38 \
--output-json /tmp/json2xml-security-benchmark.json
For Rust benchmarks, install the extension into the same environment.
uv pip install maturin
cd rust
maturin develop --release
cd ..
For native CLI benchmarks, install the external tools and verify that the commands are visible.
go install github.com/vinitkumar/json2xml-go@latest
which json2xml-go
which json2xml-zig