Lokal gemessen · v0.2.0 · Java 17

Eine vollständige EXTF-Datei mit 99.999 Zeilen.

Ein reproduzierbarer JMH-Lauf vergleicht drei streng validierte Exportpfade, erfasst die kumulative Allokation und bewahrt die Messunsicherheit, statt aus einem Laptop-Lauf eine allgemeingültige Aussage zu machen.

99.999 Buchungszeilen 33.602.459 ausgegebene Bytes 2 Forks · 10 Stichproben ohne Festplatten-I/O

Beobachtete Daten

Dieser Lauf löste keinen Zeitunterschied zwischen den beiden Plain-Pfaden auf.

Eine Operation erzeugt eine vollständige EXTF-v13-Datei aus Verwaltungssatz, 125-spaltiger Überschrift und 99.999 Buchungszeilen. In den Zeit- und Allokationsspalten ist weniger besser. Die ±-Werte sind die Halbbreiten der JMH-99,9-%-Konfidenzintervalle über zehn Messstichproben unter der Normalverteilungsannahme von JMH.

Apple M1 Pro, Temurin 17.0.19, ein Benchmark-Thread, vollständige Ausgabe je Operation
Exportpfad Zeit / Datei Abgeleitete Zeit / Zeile Abgeleitete Zeilen / Sekunde Allokierte Bytes / Datei Abgeleitete allokierte Bytes / Zeile
Plain Forward-only
DatevStreamWriter
670,193 ± 34,910 ms 6,702 µs 149.209 980.975.803 ± 10.710.036 B 9.809,86 B
Plain Retained
DatevFile
670,000 ± 15,068 ms 6,700 µs 149.252 1.011.067.254 ± 10.710.046 B 10.110,77 B
Advanced Retained
advanced.DatevFile
742,976 ± 51,878 ms 7,430 µs 134.593 966.056.224 ± 10.710.041 B 9.660,66 B
Die belastbare Aussage

Die Mittelwerte der beiden Plain-Pfade unterscheiden sich nur um 0,03 % und damit weit weniger als ihre ausgewiesene Unsicherheit; dieser Lauf trägt daher keine zeitliche Rangfolge. Forward-only allokierte in dieser Arbeitslast rund 3,0 % weniger kumulative Bytes als Plain Retained. Advanced Retained zeigte den niedrigsten Allokationswert und den höchsten beobachteten Zeitmittelwert; dieser einzelne Lauf begründet jedoch keine allgemeingültige Rangfolge.

Produktentscheidung

Zuerst den Lebenszyklus wählen, nicht die kleinste Benchmark-Zahl.

Forward-only für einmalige Exporte mit hohem Volumen

DatevStreamWriter validiert, serialisiert und übergibt jede vollständige Zeile, ohne sie in einer bibliothekseigenen Sammlung zu speichern. Der lebende, zurückgehaltene Zeilenzustand ist damit proportional zu einer Zeile statt zur Gesamtzahl. Diese Entwurfseigenschaft – keine Geschwindigkeitsbehauptung – ist der stärkste Grund für Forward-only bei einem einmaligen Export mit 99.999 Zeilen.

Retained verwenden, wenn Zeilen verfügbar bleiben müssen

Die speichernden APIs erlauben Prüfung, Iteration und verzögerte Ausgabe. Dieser Lebenszyklus ist sinnvoll, wenn der Aufrufer Zeilen erneut prüfen oder ausgeben muss; dieser Lauf löste gegenüber Forward-only keinen Zeitunterschied auf. Die ausgerichteten Zeilen bleiben zwangsläufig bis zur Freigabe des Dateiobjekts im Speicher.

Allokation ist nicht gleich belegter Heap

JMH gc.alloc.rate.norm erfasst alle während einer Operation allokierten Bytes, einschließlich kurzlebiger Validierungs- und Serialisierungsobjekte. Es misst nicht den maximal lebenden Heap. Das Advanced-Retained-Ergebnis zeigt den Unterschied: Es allokierte hier kumulativ am wenigsten, obwohl es ausgerichtete Zeilen speichert. Zur Quantifizierung des Spitzenverbrauchs wäre eine separate Live-Set- oder Heap-Occupancy-Untersuchung erforderlich.

Gemessene Arbeitslast

Eine dünn belegte, aber nicht triviale feste Zeile.

  • Format: vollständiger DATEV-Buchungsstapel / EXTF v13, Windows-1252 und CRLF.
  • Größe: 125 Spalten; 99.999 Buchungszeilen als unterstütztes Dateimaximum.
  • Belegung: sechs nicht leere Felder je Zeile: Betrag, Soll/Haben-Kennzeichen, Konto, Gegenkonto, Belegdatum und Buchungstext.
  • Maskierung: Der Buchungstext lautet Müller; Beleg "42" € und beansprucht Windows-1252-, Trennzeichen- und Anführungszeichenbehandlung.
  • Validierung: metadatenbezogene strenge semantische Validierung bei jedem Append. Plain verwendet DatevValidator, Advanced den eingebauten Modus STRICT.
  • Ausgabe: Alle Pfade werden bytegenau verglichen und erzeugen exakt 33.602.459 Bytes: 2.795 feste Verwaltungs-/Überschriftsbytes plus 336 Bytes je Buchungszeile.

Für jede Zeile wird dieselbe unveränderliche Map mit sechs Einträgen übergeben. Zeilenausrichtung, Validierung und Serialisierung liegen im Messintervall; die vorgelagerte Datensatzerzeugung und fachliche Kontierungslogik nicht.

Messvertrag

Umgebung und JMH-Konfiguration.

Gemessene Bibliothekv0.2.0 · Commit 06fa7ae
Messdatum12. August 2026
RechnerApple M1 Pro, 10 logische CPUs, 32 GiB RAM, aarch64
BetriebssystemmacOS 26.5.2 (Build 25F84)
JVMEclipse Temurin 17.0.19+10; -Xms1g -Xmx1g -XX:+AlwaysPreTouch
MessgerüstJMH 1.37, Average-Time-Modus, ein Thread, zwei Forks
IterationenJe Fork: drei × 1 s Warmup, danach fünf × 1 s Messung
ProfilerJMH gc; normalisierte Allokation in Bytes je Operation
ZielEin vorab dimensionierter, wiederverwendeter ByteArrayOutputStream, außerhalb jeder Messoperation zurückgesetzt
I/OByte-Serialisierung und Schreiben ins Ziel enthalten; Dateisystem, Netzwerk und Zielallokation ausgeschlossen

Jeder gemessene Aufruf erstellt seinen Exporter und führt alle 99.999 Appends sowie die abschließende Ausgabe aus. Unveränderliche Testdaten, Zielkapazität und der erste Bytegleichheitsvergleich werden vor der Messung vorbereitet. Die letzte Ausgabe wird nach dem Lauf erneut geprüft.

Selbst ausführen

Die Maximalzeilen-Task fixiert die wichtigen Eingaben.

git clone https://github.com/mrtyldr/datev-exporter.git
cd datev-exporter
git checkout cb91f5dd8b174bb4e10d98a8a7cc93f007483042
./gradlew --no-daemon :datev-exporter-benchmarks:jmhMaxRows

Die Task wählt die Adoptium-Java-17-Toolchain und verwendet diese JMH-Einstellungen:

-p rowCount=99999
-wi 3 -w 1s
-i 5 -r 1s
-f 2 -t 1
-jvmArgs "-Xms1g -Xmx1g -XX:+AlwaysPreTouch"
-prof gc -rf json

Das erzeugte JSON liegt unter datev-exporter-benchmarks/build/results/jmh/max-rows-gc.json. Die veröffentlichte Zusammenfassung bewahrt jede rohe Zeit- und Allokationsstichprobe:

Nicht verallgemeinern

Was dieser Lauf nicht belegt.

  • Es ist ein Lauf auf einem Entwickler-Laptop ohne dedizierte Host-Isolation, CPU-Pinning oder thermische Kontrolle.
  • Die zehn Stichproben schätzen den eingeschwungenen Zustand dieser Arbeitslast; sie sind keine Produktions-Latenzperzentile.
  • Zeilen pro Sekunde ist aus der durchschnittlichen Dateizeit berechnet und kein mehrthreadiger Durchsatztest.
  • Die dünn belegte feste Zeile bildet nicht jede Felddichte, Wertlänge, Validierungsverletzung oder vorgelagerte Objektallokation ab.
  • Das In-Memory-Ziel schließt Dateisystem, Netzwerk, Verschlüsselung, Kompression und aufruferseitige Pufferung aus.
  • Der Profiler misst kumulative Allokation, nicht maximalen RSS, maximal lebenden Heap oder Retained Size.
  • Ein Univocity-Pfad ist nicht enthalten: Der Adapter hat eine andere Ausgabegrenze und kann die hier gemessene vollständige Datei aus Verwaltungssatz und Buchungen nicht erzeugen.

Diese Zahlen dienen zum Verständnis der Implementierung und als reproduzierbare Vergleichsbasis auf der eigenen Zielhardware – nicht als SLA. Die Kompatibilitätsnachweise und Grenzen sind separat zu bewerten; Geschwindigkeit belegt keine DATEV-Importannahme.