Skip to content

Generate schema-specialized binary readers - #14

Merged
jallum merged 1 commit into
perf/alignmentfrom
perf/generated-reader
Aug 31, 2026
Merged

Generate schema-specialized binary readers#14
jallum merged 1 commit into
perf/alignmentfrom
perf/generated-reader

Conversation

@jallum

@jallum jallum commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Stacked on #13.

Summary

  • generate schema-specific binary decode clauses during use Flatbuffer expansion
  • compile table field IDs/defaults, enum and union dispatch, struct offsets, and vector element sizes into the caller
  • retain the interpreted reader for arbitrary iodata
  • track the top-level schema with @external_resource
  • cover scalar widths, strings, defaults, schema evolution, safe names, enums, unions, recursive tables, padded structs, vectors, identifiers, malformed enum values, and fallback behavior

TDD and verification

The first test traced Flatbuffer.read/2 and failed until binary reads stopped delegating to the interpreted implementation. The trace uses its own OTP trace session so it cannot interfere with ExCoveralls.

  • Elixir 1.18 / OTP 27: 131 tests, 0 failures, 2 excluded
  • Elixir 1.19 / OTP 28: 129 tests, 0 failures, 2 excluded
  • Elixir 1.20 / OTP 29 coverage: 129 tests, 0 failures, 99.4%
  • flatc interoperability: 2 tests, 0 failures
  • mix compile --warnings-as-errors
  • mix format --check-formatted
  • git diff --check

Benchee

M4 Pro, Elixir 1.19.4, OTP 28.3, 100 integers and 20 child tables:

reader average memory reductions
interpreted 7.27 µs 36.65 KB 2.11 K
generated 2.78 µs 19.77 KB 1.33 K

That is 2.62x faster with 46% less allocation.

Adversarial review

The fast path is deliberately restricted to binaries; flattening arbitrary iodata would trade hidden copying for benchmark speed. Iodata keeps the existing implementation. Generated clauses preserve compact-vtable schema evolution, unknown enum/union behavior, safe binary keys, identifier errors, and recursive table schemas.

OTP 29 exposed exponential type analysis when each generated field directly refined the row map shape. Generated code now routes puts through a small helper, keeping runtime map construction while presenting a stable type boundary to the compiler. Bitstring sizes are explicitly pinned for OTP 29 correctness warnings.

@jallum
jallum force-pushed the perf/generated-reader branch from 8e48b13 to cdfbd60 Compare August 31, 2026 18:35
@jallum
jallum merged commit af24524 into main Aug 31, 2026
4 checks passed
jallum added a commit that referenced this pull request Aug 31, 2026
Stacked on #14 (and #13).

## Summary

- generate schema-specialized reverse-builder dispatch during `use
Flatbuffer` expansion
- keep schema-independent mechanics shared in the internal
`Flatbuffer.Writer`: final framing, string pooling, vector
materialization, scalar validation, aligned pushes, and vtable
construction
- compile table fields and IDs, scalar defaults, enum/union dispatch,
struct padding, vector element sizes, and alignment into the caller
- precompute alignment-friendly table emission order while independently
assembling vtable locations in field-ID order
- preserve `to_iolist/1` and `to_binary/1`, compact/shared vtables,
shared strings, atom/binary input keys, safe schemas, recursive tables,
and existing error tuples
- extend flatc interoperability so the C++ verifier reads buffers from
both interpreted and generated writers

The shared-runtime refactor removed 291 lines (about 30%) from the
writer generator; generated code now contains only the
schema-specialized control flow and measurements.

## TDD and verification

The first test traced `Flatbuffer.to_iolist/2` and
`Flatbuffer.to_binary/2`; it failed on the original macro delegation and
passes only when both calls use generated code.

- Elixir 1.18 / OTP 27: 138 tests, 0 failures, 2 excluded
- Elixir 1.19 / OTP 28: 136 tests, 0 failures, 2 excluded
- Elixir 1.20 / OTP 29 coverage: 136 tests, 0 failures, 99.5%
- flatc interoperability: 2 tests, 0 failures; C++ verifies interpreted
and generated writer buffers
- `mix deps.unlock --check-unused`
- `mix compile --warnings-as-errors`
- `mix format --check-formatted`
- `git diff --check`

## Benchee

M4 Pro, Elixir 1.19.4, OTP 28.3, 100 integers and 20 child tables, after
shared-runtime extraction:

| writer | average | median | memory | reductions |
|---|---:|---:|---:|---:|
| interpreted | 11.45 µs | 11.21 µs | 55.45 KB | 4.62 K |
| generated | 10.30 µs | 10.08 µs | 45.80 KB | 3.56 K |

The generated writer remains about 10% faster, allocates 17.4% less
memory, and uses 22.9% fewer reductions. Both produce a 1,384-byte
representative buffer.

## flatc review and alignment ordering

flatc generates fixed `add_*` calls and groups scalar emission by wire
size/alignment. The Elixir generator follows the same useful property,
but computes true schema alignment for structs as well. Reference
objects are prepared first; inline fields are emitted by descending
alignment with a deterministic field-ID tiebreaker. Each emitted
location has its own generated local, so the vtable is assembled in ID
order without a runtime sort. This is why alignment-friendly ordering is
practical in either implementation but only wins here after the ordering
work itself is moved to compile time.

## Adversarial review

- alternate valid field order changes non-canonical bytes, so tests
compare decoded semantics and flatc verification rather than assuming
one byte layout
- sparse/absent fields still produce compact vtables because generated
location collection is ID-ordered and stops at the highest present field
- union vectors retain the existing explicit error behavior
- safe-schema code generation never interns schema field names; hygienic
locals avoid field-number-derived atoms
- shared strings/vtables, default omission, nested structs, recursive
tables, malformed scalar/enum/table/union values, binary keys, and
iolist output have dedicated parity coverage
- the module boundary was benchmarked after extraction; only
schema-dependent dispatch remains quoted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant