Skip to content

Releases: ChaseHuegel/needlefish

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 01:39

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.

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), so GetSize is never called from the serialization path and each
    node is visited once.
  • String and primitive-array copies use Buffer.MemoryCopy above a 64-byte threshold and an
    inline loop below it (memcpy call overhead dominates tiny copies).
  • The deserializer collapsed its three duplicated Unpack bodies into one
    UnpackCore(byte*, byte*) reached through a ReadOnlySpan<byte> UnpackSpan. Generated
    output is roughly half the size (SampleNsd.cs 5,074 to 2,587 lines, AddressBook.cs 2,046 to 1,263 lines)
    and JIT time drops with it.
  • Fixed:
    • GetSize threw NullReferenceException when a string[] contained a null element.
    • Big-endian deserialization mutated the caller's input buffer (including ReadOnlySpan).
      string.Create is 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
      InvalidOperationException instead 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

Choose a tag to compare

@github-actions github-actions released this 14 Oct 22:43

Full Changelog: v1.4.0...v1.5.0

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 22:12

Full Changelog: v1.3.4...v1.4.0

v1.3.4

Choose a tag to compare

@github-actions github-actions released this 14 Oct 13:58

Full Changelog: v1.3.3...v1.3.4

v1.3.3

Choose a tag to compare

@ChaseHuegel ChaseHuegel released this 14 Oct 05:13

Full Changelog: v1.3.2...v1.3.3

Fixed:

  • Malformed data can be read out of bounds #4
  • Generated Serialize methods are incurring copies #3

v1.3.2

Choose a tag to compare

@ChaseHuegel ChaseHuegel released this 22 Sep 19:10

Full Changelog: v1.3.1...v1.3.2

  • Fixed missing overload for ReadOnlySpan deserialization.

v1.3.1

Choose a tag to compare

@ChaseHuegel ChaseHuegel released this 22 Sep 19:04
  • Fixed submessage deserialization missing a Span overload.

Full Changelog: v1.3.0...v1.3.1

v1.3.0

Choose a tag to compare

@ChaseHuegel ChaseHuegel released this 11 Jul 21:31
a41ab6b
  • Changed version field to a string
  • Added --partial and -p args to nsdc to generate C# types with the partial keyword

Full Changelog: v1.2.1...v1.3.0

v1.2.1

Choose a tag to compare

@ChaseHuegel ChaseHuegel released this 22 Nov 22:38

Full Changelog: v1.2.0...v1.2.1

Fix deeply nested messages getting corrupted.

v1.2.0

Choose a tag to compare

@ChaseHuegel ChaseHuegel released this 22 May 03:13

Full Changelog: v1.1.4...v1.2.0

Added ReadOnlySpan deserialization support.