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.
| Exporter path | Time / file | Derived time / row | Derived rows / second | Allocated bytes / file | Derived allocated bytes / row |
|---|---|---|---|---|---|
Plain forward-onlyDatevStreamWriter |
670.193 ± 34.910 ms | 6.702 µs | 149,209 | 980,975,803 ± 10,710,036 B | 9,809.86 B |
Plain retainedDatevFile |
670.000 ± 15.068 ms | 6.700 µs | 149,252 | 1,011,067,254 ± 10,710,046 B | 10,110.77 B |
Advanced retainedadvanced.DatevFile |
742.976 ± 51.878 ms | 7.430 µs | 134,593 | 966,056,224 ± 10,710,041 B | 9,660.66 B |
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-inSTRICTmode. - 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 test | v0.2.0 · commit 06fa7ae |
|---|---|
| Measured | 12 August 2026 |
| Machine | Apple M1 Pro, 10 logical CPUs, 32 GiB RAM, aarch64 |
| Operating system | macOS 26.5.2 (build 25F84) |
| JVM | Eclipse Temurin 17.0.19+10; -Xms1g -Xmx1g -XX:+AlwaysPreTouch |
| Harness | JMH 1.37, average-time mode, one thread, two forks |
| Iterations | Per fork: three × 1 s warmup, then five × 1 s measurement |
| Profiler | JMH gc; normalized allocation in bytes per operation |
| Destination | A pre-sized, reusable ByteArrayOutputStream, reset outside each measured invocation |
| I/O | Byte 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.