Benchmarks

This page records one local run of the JSON codec and boundary parsing suites, last measured 2026-09-15. The numbers describe this laptop and toolchain; use them to compare paths and allocations, not as cross-machine performance guarantees.

The suites live in benchmarks/Reified.Schema.Benchmarks/CodecSuites.fs. The Benchmarks FAKE target starts a Release run and forwards any BenchmarkDotNet arguments.

Setup

The measured run used:

  • Fedora Linux 44
  • Intel Core i5-10310U, 4 physical cores / 8 logical cores
  • .NET SDK 10.0.300
  • .NET runtime 10.0.8
  • F# 10.0
  • BenchmarkDotNet 0.15.8

The recorded results use BenchmarkDotNet's ShortRun job: one launch, three warmups, and three measured iterations. That is enough for a laptop-local directional comparison, but the error bars are wide — often a large fraction of the mean — so treat close timings as equivalent until a longer run shows otherwise. Allocations are stable between runs and are the more reliable signal here.

JSON codec

The codec suite measures Reified.Schema's Json codec on the same aggregate and representation as System.Text.Json: string against string, and UTF-8 bytes against UTF-8 bytes. The model has seven primitive fields, one nested record, two collections, and boundary constraints on name and age.

Run them:

BENCHMARK_ARGS='--job short --filter *' dotnet run --project tools/Reified.Build -- --target Benchmarks
Operation Reified mean / allocated System.Text.Json mean / allocated
Serialize string 1.33 us / 1,232 B 1.36 us / 1,136 B
Serialize UTF-8 1.28 us / 880 B 1.31 us / 776 B
Deserialize string 3.10 us / 2,408 B 2.88 us / 2,056 B
Deserialize UTF-8 2.42 us / 2,016 B 2.99 us / 2,056 B

The canonical-order UTF-8 decoder was 19% faster than System.Text.Json in this run and allocated 40 B less. String decoding includes UTF-8 conversion and remained slower. The integer encoder's direct formatting keeps Reified serialization within 96–104 B of System.Text.Json. The UTF-8 APIs avoid the string representation and are the relevant comparison when a payload already arrives as bytes.

Codec compilation is separate from per-payload work:

Operation Mean Allocated
Json.compile for the customer schema 14.19 us 20.45 KB

Compile a codec once and reuse it. Recompiling per payload would dominate the encode path and cost roughly two to three times one decode on this model.

Boundary parsing

The boundary suite compares the trusted codec against full boundary parsing — JsonDocument to Data to Schema.parse with complete path-aware diagnostics:

Operation Mean Allocated
Reified Json.deserializeBytes (trusted, end to end) 2.91 us 1.97 KB
JsonDocument + Data + Schema.parse (boundary, end to end) 11.87 us 13.22 KB

The boundary path was 4.1 times the mean and 6.7 times the managed allocation of trusted UTF-8 decoding in this run. Stage measurements show where that work lands:

Boundary stage Mean Allocated
JSON document to Data 3.08 us 3.56 KB
Schema.parse invalid Data 7.85 us 9.21 KB
Schema.parse valid Data 8.22 us 9.66 KB

The stages are diagnostic measurements, not additive accounting: the end-to-end benchmark measures its own complete operation. The invalid case changes the constrained name to an empty string and measures accumulated error creation.

Alias resolution uses the same customer shape, with Name accepted as an input-only alias for canonical name:

Alias operation Mean Allocated
Compiled JSON decode through Name 3.43 us 2.59 KB
Schema.parse through Name 8.20 us 9.66 KB
Reject canonical name plus alias Name 7.13 us 8.80 KB

Canonical and alias Schema.parse timings are indistinguishable within this short run. Alias JSON decoding takes the general fallback path and therefore retains its field slots; ambiguity detection exits before value parsing completes.

Scaling cases

The wide-record suite isolates field dispatch and per-field decode state with 24 integer fields:

Operation Reified mean / allocated System.Text.Json mean / allocated
Serialize UTF-8 0.74 us / 320 B 0.71 us / 232 B
Deserialize UTF-8 1.70 us / 1,072 B 2.11 us / 744 B

The integer formatting keeps serialization allocation close even as field count grows. Canonical-order decoding avoids field dispatch and per-field slots; it was 19% faster than System.Text.Json here, while allocating 328 B more because applying the curried constructor still creates intermediate closures. Reordered or aliased objects use the general unordered decoder.

The list suite measures a root int list without record-field dispatch:

Items Reified serialize System.Text.Json serialize Reified deserialize System.Text.Json deserialize
10 0.21 us / 80 B 0.23 us / 88 B 0.31 us / 640 B 0.49 us / 576 B
1,000 17.65 us / 3,952 B 19.02 us / 3,960 B 33.23 us / 64,000 B 39.76 us / 40,464 B
10,000 189.34 us / 48,952 B 153.86 us / 48,960 B 388.81 us / 640,000 B 417.31 us / 451,440 B

Serialization allocation is effectively equal across list sizes. Timing leads varied by list size and remain within wide short-run error bars, so a longer job is required before treating either serializer as the throughput winner. Reified list decoding is competitive on time but allocates more per item.

Conclusion

  • For trusted canonical UTF-8 payloads, Reified decoded both the representative aggregate and the 24-field record about 19% faster than System.Text.Json in this run.
  • For untrusted input, boundary parsing costs more because it builds Data, checks constraints, and accumulates path-aware diagnostics.
  • Wide records identify decode slots and field matching as the next optimization target; large lists identify per-item decode allocation as a separate scaling target.
  • Compile each codec once and reuse it so compilation stays outside the per-payload path.