Repository navigation
Releases: ChaseHuegel/needlefish
Release list
v1.6.0
This was a performance focused update with some minor corrections, and significant
reduction in generated code verbosity. Benchmarks were introduced and executed before
and after the changes. Serialization and deserialization both had gains, but serialization
by far had the biggest improvements. See the results below.
Environment
AMD Ryzen 5 3600X 4.41GHz, 1 CPU, 12 logical / 6 physical cores, Ubuntu 26.04.1 LTS.
.NET 10.0.12 (X64 RyuJIT AVX2), SDK 10.0.112, BenchmarkDotNet 0.15.2, DefaultJob.
Exhaustivebenchmarks refer to an nsd message containing all supported types and fields.Nestedbenchmarks refer to an nsd message containing 3 layers of nested nsd submessages.Primitivebenchmarks refer to an nsd message containing all supported primitive types.
Before - Needlefish v3.3.0 / nsdc v1.5.0 / Generators v1.2.0
| Type | Method | People | StringLength | Mean | Allocated |
|---|---|---|---|---|---|
| DeserializePrimitivesBenchmarks | Unpack | - | - | 28.33 ns | - |
| DeserializePrimitivesBenchmarks | Deserialize | - | - | 30.89 ns | - |
| SerializeNestedBenchmarks | GetSize | 16 | - | 111.83 ns | - |
| SerializeNestedBenchmarks | SerializeInto | 16 | - | 951.35 ns | - |
| SerializeNestedBenchmarks | Serialize | 16 | - | 1,262.31 ns | 3448 B |
| SerializeNestedBenchmarks | GetSize | 128 | - | 935.66 ns | - |
| SerializeNestedBenchmarks | SerializeInto | 128 | - | 7,441.16 ns | - |
| SerializeNestedBenchmarks | Serialize | 128 | - | 10,286.51 ns | 27752 B |
| SerializeExhaustiveBenchmarks | GetSize | - | 16 | 13.76 ns | - |
| SerializeExhaustiveBenchmarks | SerializeInto | - | 16 | 72.57 ns | - |
| SerializeExhaustiveBenchmarks | Serialize | - | 16 | 146.25 ns | 416 B |
| SerializeExhaustiveBenchmarks | GetSize | - | 1024 | 13.50 ns | - |
| SerializeExhaustiveBenchmarks | SerializeInto | - | 1024 | 1,480.25 ns | - |
| SerializeExhaustiveBenchmarks | Serialize | - | 1024 | 1,829.47 ns | 4448 B |
| DeserializeExhaustiveBenchmarks | Unpack_Fresh | - | 16 | 300.15 ns | 640 B |
| DeserializeExhaustiveBenchmarks | Unpack_Reused | - | 16 | 310.43 ns | 640 B |
| DeserializeExhaustiveBenchmarks | Deserialize | - | 16 | 324.69 ns | 640 B |
| DeserializeExhaustiveBenchmarks | Unpack_Fresh | - | 1024 | 545.12 ns | 4672 B |
| DeserializeExhaustiveBenchmarks | Unpack_Reused | - | 1024 | 636.42 ns | 4672 B |
| DeserializeExhaustiveBenchmarks | Deserialize | - | 1024 | 594.05 ns | 4672 B |
After - Needlefish v3.4.0 / nsdc v1.6.0 / Generators v1.3.0
| Type | Method | People | StringLength | Mean | Allocated |
|---|---|---|---|---|---|
| DeserializePrimitivesBenchmarks | Unpack | - | - | 22.17 ns | - |
| DeserializePrimitivesBenchmarks | Deserialize | - | - | 22.72 ns | - |
| SerializeNestedBenchmarks | GetSize | 16 | - | 103.78 ns | - |
| SerializeNestedBenchmarks | SerializeInto | 16 | - | 717.19 ns | - |
| SerializeNestedBenchmarks | Serialize | 16 | - | 1,058.82 ns | 3448 B |
| SerializeNestedBenchmarks | GetSize | 128 | - | 945.08 ns | - |
| SerializeNestedBenchmarks | SerializeInto | 128 | - | 5,956.72 ns | - |
| SerializeNestedBenchmarks | Serialize | 128 | - | 7,936.31 ns | 27752 B |
| SerializeExhaustiveBenchmarks | GetSize | - | 16 | 12.57 ns | - |
| SerializeExhaustiveBenchmarks | SerializeInto | - | 16 | 62.97 ns | - |
| SerializeExhaustiveBenchmarks | Serialize | - | 16 | 97.15 ns | 416 B |
| SerializeExhaustiveBenchmarks | GetSize | - | 1024 | 12.54 ns | - |
| SerializeExhaustiveBenchmarks | SerializeInto | - | 1024 | 89.58 ns | - |
| SerializeExhaustiveBenchmarks | Serialize | - | 1024 | 291.56 ns | 4448 B |
| DeserializeExhaustiveBenchmarks | Unpack_Fresh | - | 16 | 260.72 ns | 640 B |
| DeserializeExhaustiveBenchmarks | Unpack_Reused | - | 16 | 296.47 ns | 640 B |
| DeserializeExhaustiveBenchmarks | Deserialize | - | 16 | 274.07 ns | 640 B |
| DeserializeExhaustiveBenchmarks | Unpack_Fresh | - | 1024 | 473.87 ns | 4672 B |
| DeserializeExhaustiveBenchmarks | Unpack_Reused | - | 1024 | 468.97 ns | 4672 B |
| DeserializeExhaustiveBenchmarks | Deserialize | - | 1024 | 529.37 ns | 4672 B |
Comparison
| Benchmark | Before | After | Change |
|---|---|---|---|
Exhaustive SerializeInto, 1024 char |
1,480.25 ns | 89.58 ns | -94% |
Exhaustive Serialize, 1024 char |
1,829.47 ns | 291.56 ns | -84% |
Exhaustive Serialize, 16 char |
146.25 ns | 97.15 ns | -34% |
Nested SerializeInto, 128 people |
7,441.16 ns | 5,956.72 ns | -20% |
Nested Serialize, 128 people |
10,286.51 ns | 7,936.31 ns | -23% |
Nested SerializeInto, 16 people |
951.35 ns | 717.19 ns | -25% |
Nested Serialize, 16 people |
1,262.31 ns | 1,058.82 ns | -16% |
Exhaustive SerializeInto, 16 char |
72.57 ns | 62.97 ns | -13% |
Exhaustive GetSize |
~13.6 ns | ~12.6 ns | -8% |
Nested GetSize |
~1,045 ns | ~1,024 ns | ~2% |
Exhaustive Unpack_Fresh |
300.15 / 545.12 ns | 260.72 / 473.87 ns | -13% |
Exhaustive Unpack_Reused |
310.43 / 636.42 ns | 296.47 / 468.97 ns | -5% / -26% |
Exhaustive Deserialize |
324.69 / 594.05 ns | 274.07 / 529.37 ns | -16% / -11% |
Primitives Unpack / Deserialize |
28.33 / 30.89 ns | 22.17 / 22.72 ns | -22% / -26% |
What changed
- The serializer writes nested object lengths by patching the length prefix after writing the
child (SerializeCore), soGetSizeis never called from the serialization path and each
node is visited once. - String and primitive-array copies use
Buffer.MemoryCopyabove a 64-byte threshold and an
inline loop below it (memcpy call overhead dominates tiny copies). - The deserializer collapsed its three duplicated
Unpackbodies into one
UnpackCore(byte*, byte*)reached through aReadOnlySpan<byte>UnpackSpan. Generated
output is roughly half the size (SampleNsd.cs5,074 to 2,587 lines,AddressBook.cs2,046 to 1,263 lines)
and JIT time drops with it. - Fixed:
GetSizethrewNullReferenceExceptionwhen astring[]contained a null element.- Big-endian deserialization mutated the caller's input buffer (including
ReadOnlySpan).
string.Createis now used for the swap path. - Big-endian float/double serialization converted the byte-swapped bits numerically instead
of reinterpreting them. - Optional objects deserialized via
OptionalSubmessage.Value.Unpack(...), which mutated a
temporary value. They now unpack into a local value and assign back. - Values above 65,535 bytes (strings, arrays, nested objects) now throw
InvalidOperationExceptioninstead of silently truncating the length prefix. - The README incorrectly stated enum values were 16-bit signed integers on the wire.
Now it correctly states they are 32-bit.
Full Changelog: v1.5.0...v1.6.0
v1.5.0
v1.4.0
v1.3.4
v1.3.3
Full Changelog: v1.3.2...v1.3.3
Fixed:
v1.3.2
Full Changelog: v1.3.1...v1.3.2
- Fixed missing overload for ReadOnlySpan deserialization.
v1.3.1
- Fixed submessage deserialization missing a Span overload.
Full Changelog: v1.3.0...v1.3.1
v1.3.0
- Changed
versionfield to a string - Added
--partialand-pargs tonsdcto generate C# types with thepartialkeyword
Full Changelog: v1.2.1...v1.3.0
v1.2.1
Full Changelog: v1.2.0...v1.2.1
Fix deeply nested messages getting corrupted.
v1.2.0
Full Changelog: v1.1.4...v1.2.0
Added ReadOnlySpan deserialization support.