[AvroSerializable] types vs generated code (#31)
This run measures the serializers of a C# type marked [AvroSerializable] (the attribute-driven generator, #31). They are compared on the same record with the type generated from .avsc, the generic model, and Apache.Avro's generic model. The exit criterion is that the typed path beats Apache.Avro. The attribute-driven generator emits through the .avsc generator's serializer code, so it should also match generated code. Measured on 2026-10-01.
| CPU | AMD EPYC 7543 (Zen 3), 2 sockets, x86-64-v3 |
| OS / runtime | Ubuntu 22.04, .NET 10.0.12 (SDK 10.0.401) |
| Code | baacd05 (the head of #181 at the time) |
| Job | DefaultJob, one 4-core lane (one CCD) per socket, run at the same time |
| Benchmark | GenericRecordBenchmarks |
The workload: one Order record with:
- a
longid, a string, a double, an int and a boolean; - a nullable string;
- 10 nested
Linerecords; - 64
longcounters; - a 2-entry map.
How the twin is set up: the [AvroSerializable] twin is bench/AvroSharp.Benchmarks/AttributedTypes.cs, with the same fields as Schemas/order.avsc, written as C# properties with camelCase field names. Setup checks that it reproduces the generic encoding byte for byte, as it does for the generated type.
Results
Times are per record, for socket 0 / socket 1. Allocations are the same on both.
| Row | Read | Allocated | Write | Allocated |
|---|---|---|---|---|
| Apache.Avro (baseline) | 2,513 / 2,536 ns | 5,032 B | 2,270 / 2,454 ns | 5,608 B |
| Generic model | 800 / 775 ns | 2,912 B | 504 / 539 ns | 0 B |
Generated from .avsc |
553.5 / 557.6 ns | 2,048 B | 316.0 / 316.7 ns | 0 B |
[AvroSerializable] |
526.8 / 536.6 ns | 2,048 B | 316.9 / 331.8 ns | 0 B |
Findings
- Against Apache.Avro:
- Reads are 4.7–4.8× faster.
- Writes are 7.2–7.4× faster.
- The type allocates 41% of Apache.Avro's memory per read, and nothing per write.
- So the #31 exit criterion holds on this machine.
- Against generated code:
- Reads are 3.8–4.8% faster, on both sockets.
- Writes are the same on socket 0 (316.9 against 316.0 ns), and 4.8% slower on socket 1 (331.8 against 316.7 ns).
- The serializers are the same code, so the difference is code and heap placement within the lanes' noise, which is 3–5% between lanes of one build.
- Allocations are identical.
- The generated code is the same: apart from the type's own declarations, the attribute-driven generator emits through
CSharpCodeGenerator.GenerateDeclared, which is the.avscpath'sEmitRecordwithout the property declarations.
The rows are gated like the others (AvroSharp_Attributed_Read and AvroSharp_Attributed_Write against Apache.Avro's baseline). So every full gate run checks this result again.