XML CLI benchmark · 4 августа 2026

Большой XML.
Честный latency.

34 реализации на одном файле размером 2,32 MB. По 2000 запусков, строгая проверка malformed XML и отдельный namespace-контракт.

29
вариантов в рейтинге
2000
наблюдений на строку
8.748
ms · лучший mean
CPU 0
две чистые серии

Практический выбор

Что ставить в CI

Mean определяет место. p99 и p99.9 показывают хвост, который среднее скрывает.

01без namespaces

TurboXml NativeAOT

8.748 ms mean

Самый быстрый вариант, если namespace semantics не входят в контракт.

Median
8.223
p99
14.495
p99.9
18.560
02namespaces ✓

XmlReader NativeAOT

10.112 ms mean

Лучший wrapper для полного namespace-контракта. В измеренной конфигурации DTD запрещены.

Median
9.612
p99
15.903
p99.9
20.562

29 strict-core вариантов

Полный рейтинг

Все значения — миллисекунды. SD — выборочное стандартное отклонение.

полный namespace-контракт только strict core Рейтинг: арифметическое mean ↑

Таблица прокручивается по горизонтали →

Open-source XML CLI и wrapper-варианты, измеренные на XML fixture размером 2,319,739 bytes
Парсер / CLI Deployment и core Контракт Лицензия Min Mean Median p90 p95 p99 p99.9 SD Загрузка
1TurboXml 2.1.0C# · .NET 10 NativeAOT · streaming SAXstrict coreBSD-26.9608.7488.22310.91611.89114.49518.5601.603NuGet ↗
2System.Xml.XmlReaderC# · .NET 10 NativeAOT · streaming pullnamespaces ✓MIT7.86310.1129.61212.47513.53615.90320.5621.793.NET ↗
3Expat 2.8.2 · xmlwf -nC · Expat · streamingnamespaces ✓MIT10.57113.42612.84615.89917.23120.75924.0122.022Releases ↗
4Expat 2.8.2 · xmlwfC · Expat · streamingstrict coreMIT10.72213.74313.09116.72217.96021.56024.5772.192Releases ↗
5RXP 1.5.0 · -xsC · RXPstrict coreGPL-2.011.99115.54214.69118.99421.20627.65637.2363.116Upstream ↗
6RXP 1.5.0 · -NxsC · RXPnamespaces ✓GPL-2.012.23217.02215.93421.78624.43831.34651.4574.001Upstream ↗
7libxml2 2.9.14 · --streamC · libxml2strict coreMIT13.72017.23116.61220.57021.95425.02129.3482.438Releases ↗
8XMLStarlet 1.6.1C · libxml2strict coreMIT13.87618.08717.31822.04223.97328.35334.9043.019Download ↗
9Xerces-C 3.2.4 · -nC++ · SAX2 · namespaces disabledstrict coreApache-2.017.46622.52021.75726.73228.87433.51837.2843.158Download ↗
10Xerces-C 3.2.4C++ · SAX2 · namespace processingnamespaces ✓Apache-2.018.22623.60522.85428.05529.90334.52638.2333.298Download ↗
11sclable/xml-lint 0.9.0PHP 8.4 · DOM · libxml2namespaces ✓MIT45.86556.70655.50964.65168.09675.69895.4966.342Packagist ↗
12TurboXml 2.1.0C# · .NET 10 JIT · streaming SAXstrict coreBSD-250.20066.55164.60376.36781.71992.017106.2278.038NuGet ↗
13System.Xml.XmlReaderC# · .NET 10 JIT · streaming pullnamespaces ✓MIT51.42366.99465.98975.98179.95085.823115.1297.209.NET ↗
14mickaelandrieu/xml-linter 2.0.0PHP 8.4 · DOM · libxml2strict coreMIT55.68768.07466.90676.88880.28988.451104.9546.728Packagist ↗
15JDK StAXGraalVM CE 25.2.4 · native · streaming cursornamespaces ✓GPL-2 + CE72.55382.81482.39087.46089.72697.182103.5674.068GraalVM ↗
16JDK SAXGraalVM CE 25.2.4 · native · streaming eventsnamespaces ✓GPL-2 + CE73.20383.08382.67887.43389.48996.044112.1064.539GraalVM ↗
17anyxml-cli 0.5.0Rust · anyxml 0.13.0strict coreMIT74.44689.69888.45799.902103.399112.467134.3328.012crates.io ↗
18JAXB RI 4.0.9 NativeGraalVM CE 25.2.4 · JDK SAX + JAXBnamespaces ✓EDL/BSD-379.01196.51594.793107.941112.867127.174155.0629.646GraalVM ↗
19Woodstox 7.2.1GraalVM CE 25.2.4 · native · streaming StAXnamespaces ✓Apache-2.090.527103.047102.617108.010110.536118.821134.6314.668Releases ↗
20xmltodict 1.0.4Python 3.13 · pyexpatstrict coreMIT115.697138.724135.937153.442161.614183.983223.88613.051PyPI ↗
21kislyuk/yq 4.1.2 · xqPython · xmltodict + jqstrict coreApache-2.0227.569263.784261.367286.390295.802323.214341.29818.012PyPI ↗
22JDK StAXOpenJDK 21.0.11 · JIT · streaming cursornamespaces ✓GPL-2 + CE253.055292.906291.212314.934322.301338.550381.09216.954OpenJDK ↗
23JDK SAXOpenJDK 21.0.11 · JIT · streaming eventsnamespaces ✓GPL-2 + CE265.816314.299311.999339.043347.541365.017381.39118.538OpenJDK ↗
24Woodstox 7.2.1OpenJDK 21.0.11 · JIT · streaming StAXnamespaces ✓Apache-2.0315.649368.448367.421395.636402.893417.761444.93820.216Releases ↗
25XML::Twig 3.52Perl · XML::Parser + Expatstrict coreArtistic/GPL473.756555.101548.583606.071634.026696.985772.45942.246MetaCPAN ↗
26JAXB RI 4.0.9 JVMJava 21 · JDK SAX + JAXBnamespaces ✓EDL/BSD-3644.205737.767732.928792.378818.895892.022985.51246.131Maven ↗
27SaxonHE12NetXQuery 12.9.10.NET 10 · Saxon-HE via IKVMnamespaces ✓MPL-2.0933.3121040.6241036.0451099.3781121.0451178.2591347.42348.404NuGet ↗
28SaxonJ-HE 12.10Java 21 · Saxonnamespaces ✓MPL-2.01108.9231282.1921273.7631380.9641414.3441486.7131576.16175.003Download ↗
29BaseX 12.4Java 21 · BaseX + JDK SAXnamespaces ✓BSD-31221.0311462.7791452.1111565.4921607.3581706.8771858.79481.154Download ↗

Измерено отдельно

Без места в рейтинге

AngleSharp и Aalto нарушили correctness-контракт; DOM-режим xmllint дублирует тот же parser core.

СтатусВариантMinMeanMedianp90p95p99p99.9SDПричина
исключёнAngleSharp.Xml NativeAOT71.15886.37184.79096.724101.543113.141133.8548.474Принял запрещённый U+0001
исключёнAngleSharp.Xml JIT261.431303.727299.987330.977342.988376.891431.94122.445Принял запрещённый U+0001
исключёнAalto 1.4.0 Native89.023100.409100.083105.477107.762112.527123.3564.290Принял overlong UTF-8 и >U+10FFFF
исключёнAalto 1.4.0 JIT260.031299.133297.198320.963328.525345.062365.52116.607Принял overlong UTF-8 и >U+10FFFF
controlxmllint DOM18.31322.59821.79926.80728.23731.52935.8202.955Тот же libxml2, что у ranked stream

Цена запуска процесса

Native действительно меняет картину

Каждый hyperfine run запускает новый CLI-процесс. Это cold CLI latency, а не throughput долгоживущего сервиса.

7.61×

TurboXml

66.551 ms JIT → 8.748 ms NativeAOT

6.63×

XmlReader

66.994 ms JIT → 10.112 ms NativeAOT

7.64×

JAXB

737.767 ms JVM → 96.515 ms GraalVM

3.54×

JDK StAX

292.906 ms JIT → 82.814 ms GraalVM

3.78×

JDK SAX

314.299 ms JIT → 83.083 ms GraalVM

3.58×

Woodstox

368.448 ms JIT → 103.047 ms GraalVM

До измерения скорости

Correctness-контракт

Быстрый parser без надёжного ненулевого exit code не является CI gate.

01

Strict core

Два корректных XML должны завершиться с кодом 0. Восемь malformed fixtures — с ненулевым.

  • tag mismatch
  • unknown entity
  • два корня
  • усечённый документ
  • duplicate attribute
  • текст вне root
  • пустой input
  • запрещённый U+0001

Для Java addendum добавлены external DTD, overlong UTF-8 и code point выше U+10FFFF. Aalto принял два последних и исключён.

02

Namespaces

Отдельно проверены unbound prefix, duplicate expanded name и запрещённая привязка префикса xml.

Полный контракт прошли 17 ranked-строк:

XmlReader AOT/JIT · xmlwf -n · RXP -Nxs · Xerces-C namespaces · PHP sclable · JDK SAX/StAX AOT/JIT · Woodstox AOT/JIT · JAXB Native/JVM · Saxon .NET/J · BaseX

03

DTD — policy, не malformed

Legal internal DTD проверялся отдельно. Java wrappers addendum работают в DTD-prohibited режиме: DTD и external entities отключены, resolver запрещает external access.

Это policy для CI gate, не DTD validation. XmlReader, TurboXml, PHP mickaelandrieu, xmltodict и xq также намеренно отвергли legal DTD.

Воспроизводимость

Методология замеров

Один вход, один CPU и одинаковые hyperfine-настройки; baseline и Java addendum измерены последовательно.

  1. 01

    Вход

    XML fixture, 2,319,739 bytes. Содержимое, внутреннее имя и hash публично не раскрываются.

  2. 02

    Две серии

    Неизменённый baseline из 26 строк и отдельный Java addendum из 8. Старые команды повторно не запускались.

  3. 03

    Каждая серия

    hyperfine: 10 warmup, 1000 запусков forward и 1000 в строго обратном порядке. N=2000 на команду.

  4. 04

    Изоляция

    Docker Desktop x86_64, AMD Ryzen 7 5700X3D, cpuset=0, сеть отключена; benchmark — главный процесс контейнера.

Median

Центральное наблюдение pooled-серии. Устойчивее mean к единичным длинным паузам.

p90 / p95 / p99 / p99.9

Hyndman–Fan type 7: h = (n − 1) × p, линейная интерполяция между соседними значениями.

Sample SD

Выборочное стандартное отклонение с делителем n − 1 по всем 2000 raw times.

Технические детали воспроизведения
  • Baseline и Java addendum запускались в двух чистых benchmark images; точные digests сохранены с локальными артефактами.
  • Публичный Java Dockerfile принимает baseline через BASE_IMAGE; локальное имя image намеренно не раскрывается.
  • --shell=none для всех команд, кроме stdin-only wrapper-а xmltodict.
  • CLI/runtime startup входит во время; Docker startup и build time не входят. Каждый JIT run запускает новую cold one-shot JVM — 10 warmups не сохраняют JVM между наблюдениями.
  • Java JIT: OpenJDK 21.0.11+10 Debian. Native: GraalVM CE 25.2.4 / Java 25.0.4+7. Aalto 1.4.0, Woodstox 7.2.1.
  • Предыдущая частичная серия полностью отброшена после обнаружения in-container polling.
  • Forward/reverse объединены отдельно по каждой команде, выбросы сохранены; затем все допущенные строки встроены в один mean-рейтинг.
  • Raw JSON, correctness TSV, minimal wrappers и скрипты опубликованы ниже без фильтрации.

Проверить самостоятельно

Данные и скрипты

Числа на странице автоматически сверяются с combined JSON перед публикацией.

Минимальные Java wrappers и build metadata