Measured locally · v0.2.0 · Java 17

A complete 99,999-row EXTF file.

A reproducible JMH run compares three strictly validated exporter paths, records cumulative allocation, and preserves the uncertainty instead of turning one laptop run into a universal claim.

99,999 booking rows 33,602,459 bytes emitted 2 forks · 10 samples disk I/O excluded

Observed data

This run did not resolve a timing difference between the two plain paths.

One operation creates a complete EXTF v13 file containing the management record, the 125-column heading and 99,999 booking rows. Lower is better in the time and allocation columns. The ± values are JMH 99.9% confidence-interval half-widths across ten measurement samples, under JMH’s normality assumption.

Apple M1 Pro, Temurin 17.0.19, one benchmark thread, complete output per operation
Exporter path Time / file Derived time / row Derived rows / second Allocated bytes / file Derived allocated bytes / row
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
The defensible takeaway

The two plain means differ by only 0.03%, far below their reported uncertainty, so this run supports no timing ranking between them. Forward-only allocated about 3.0% fewer cumulative bytes than plain retained in this workload. Advanced retained had the lowest allocation score and the highest observed mean time, but this single run is not evidence of a universal ranking.

Product decision

Choose the lifecycle first, not the smallest benchmark number.

Use forward-only for one-pass, high-volume export

DatevStreamWriter validates, serializes and hands off each completed row without retaining it in a library-owned collection. Its live retained row state is therefore proportional to one row rather than the total row count. That design property—not a speed claim—is the strongest reason to prefer it for a 99,999-row one-pass export.

Use retained output when rows must remain available

The retained APIs support inspection, iteration and delayed writing. That lifecycle is useful when the caller must review or replay rows; this run did not resolve a timing difference versus forward-only. It necessarily keeps aligned rows alive until the file object can be released.

Allocation is not retained heap

JMH’s gc.alloc.rate.norm reports all bytes allocated during an operation, including short-lived validation and serialization objects. It does not report peak live heap. The advanced retained result demonstrates the distinction: it allocated the fewest cumulative bytes here even though it still retains the aligned rows. A separate live-set or heap-occupancy study would be required to quantify peak memory.

What was measured

A sparse but non-trivial fixed row.

  • Format: complete DATEV Buchungsstapel / EXTF v13, Windows-1252 and CRLF.
  • Size: 125 columns; 99,999 booking rows, the supported per-file maximum.
  • Density: six non-empty fields per row: amount, debit/credit marker, account, contra account, document date and booking text.
  • Escaping: booking text is Müller; Beleg "42" €, exercising Windows-1252, delimiter and quote escaping.
  • Validation: metadata-aware strict semantic validation on every append. Plain paths use DatevValidator; advanced uses built-in STRICT mode.
  • Output: all paths are checked byte-for-byte and emit exactly 33,602,459 bytes: 2,795 fixed management/heading bytes plus 336 bytes per booking row.

The same immutable six-entry map is submitted for each row. Row alignment, validation and serialization remain inside the timed operation; upstream record creation and accounting-data mapping do not.

Measurement contract

Environment and JMH configuration.

Library under testv0.2.0 · commit 06fa7ae
Measured12 August 2026
MachineApple M1 Pro, 10 logical CPUs, 32 GiB RAM, aarch64
Operating systemmacOS 26.5.2 (build 25F84)
JVMEclipse Temurin 17.0.19+10; -Xms1g -Xmx1g -XX:+AlwaysPreTouch
HarnessJMH 1.37, average-time mode, one thread, two forks
IterationsPer fork: three × 1 s warmup, then five × 1 s measurement
ProfilerJMH gc; normalized allocation in bytes per operation
DestinationA pre-sized, reusable ByteArrayOutputStream, reset outside each measured invocation
I/OByte serialization and destination writes included; filesystem, network and destination allocation excluded

Each measured call constructs its exporter and performs all 99,999 appends plus final output. Immutable fixture construction, output capacity allocation and initial byte-equivalence checks happen at trial setup. The final invocation is checked again at trial teardown.

Run it yourself

The maximum-row task pins the important inputs.

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

The task selects the Adoptium Java 17 toolchain and expands to the following JMH settings:

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

Generated JSON is written to datev-exporter-benchmarks/build/results/jmh/max-rows-gc.json. The published summary retains every raw timing and allocation sample:

Do not over-generalize

What this run cannot establish.

  • It is one run on one developer laptop, without dedicated-host isolation, CPU pinning or thermal controls.
  • The ten samples estimate steady-state behavior for this fixture; they are not production latency percentiles.
  • Derived rows/second is arithmetic from average file time, not a multi-threaded throughput test.
  • The sparse fixed row does not model every field density, value length, validation failure or upstream object-allocation pattern.
  • The in-memory destination excludes filesystem, network, encryption, compression and caller-selected buffering costs.
  • The profiler measures cumulative allocation, not maximum RSS, peak live heap or retained object size.
  • No Univocity path is included: the adapter has a different output boundary and cannot produce the complete management-record-plus-bookings file measured here.

Use these figures to understand this implementation and to reproduce a comparison on your deployment hardware—not as an SLA. Review the compatibility evidence and limits separately; speed does not prove DATEV import acceptance.