Skip to content

Releases

v4.0.0-rc.9

v4.0.0-rc.9 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 04:06

Install: dotnet add package Nino --version 4.0.0-rc.9 (Unity package: 4.0.0-preview.209)

Other languages: go get github.com/JasonXuDeveloper/Nino/runtimes/go/v4@v4.0.0-rc.9, cargo add nino-serialization@=4.0.0-rc.9, npm install nino-serialization@4.0.0-rc.9.

New

  • NinoHeader.Presence replaces NinoHeader.NullFlag, and published layouts say header=Presence (NinoTypeLayoutAttribute.ToString(), the Go and Rust layout strings); NullFlag is removed.
  • NinoPolymorphic.TryRegister<T>() and TryRegister(Type) replace Register<T>() and Register(Type), which are removed; they return false when the type cannot be registered.
  • NinoDeserializer.Deserialize<T>(ref Reader reader, ref T value) and DeserializeBoxed(ref Reader reader, ref object value, Type type) take the reader first, like every other read; the value-first overloads are removed.
  • [NinoType(inheritedBySubtypes: false)] replaces allowInheritance: false (and NinoTypeAttribute.InheritedBySubtypes replaces AllowInheritance); the old name is removed, and a code fix renames a named allowInheritance: argument.
  • Analyzer warning NINO030 reports a [NinoUtf8] that has no effect (no string in the member's type, or a [NinoMemberFormatter] writes it), with a code fix that removes it.
  • [NinoMemberFormatter(typeof(Utf8StringFormatter))] on a string member is now read as [NinoUtf8] (same bytes, utf8 in the published layout); info NINO031 and its code fix rewrite it to [NinoUtf8].
  • WEAK_VERSION_TOLERANCE is removed. Where a project still defines it, error NINO032 marks each type that would turn positional, and its code fix adds format: NinoFormat.VersionTolerant, which keeps the same bytes (fix all applies it to the whole solution).
  • Published layouts write the type id as id=0x... (was typeId=), and NinoTypeLayoutAttribute's constructor parameter is id, matching its Id property; Go and Rust layout strings use id= too.
  • Custom formatters implement Deserialize(ref Reader reader, ref T value): the reader comes first, as in NinoDeserializer; the old order is removed, so swap the two parameters in your formatters.
  • The code fix for [NinoType(containNonPublicMembers: ...)] (the v3 name) renames it to includeNonPublicMembers.
  • Warning NINO033 reports [NinoUtf8], [NinoMemberFormatter] and [NinoIgnore] on an interface member, where they have no effect (implementing types write their own member with its own attributes); its code fix moves the attribute to the implementing members.
  • Rust: the type id nino-gen declares on each generated type is now NINO_TYPE_ID (was TYPE_ID), named like NINO_TYPE_NAME and NINO_LAYOUT; regenerate, and write Cat::NINO_TYPE_ID where code named Cat::TYPE_ID (the body traits' TYPE_ID is unchanged).

Performance

  • In the IDE, an edit that changes no declaration no longer makes the source generator bind every top-level Serialize/Deserialize call again, so typing in a project with many such calls costs the generator much less.
  • Reading a list of plain structs with NinoSettings.Untrusted checks a bool field in the same SIMD pass as its neighbours: 11-20 % faster on x64 including rc.9's over-read fix (EPYC 7763: 2.70 → 2.40 µs for 1,000 values; EPYC 9V45: 1.40 → 1.11 µs; runs 42341861781 and 42365821207), on par on Arm64.

Fixes

  • NINO007 and NINO029 are reported once for a member of a base type instead of once per subtype; NINO030 now reports [NinoUtf8] on members that are never serialized and on interface members, where it has no effect.
  • An explicit implementation of a [NinoType] interface's property (int IFoo.X { get; set; }) no longer breaks the build with CS1061: it is serialized through the interface, without boxing structs, with the interface member's [NinoUtf8], [NinoMemberFormatter] and [NinoIgnore], in the same layout as an implicit implementation; NINO033 no longer reports those attributes where such an implementation makes them take effect.
  • Reading a Dictionary of plain struct values with NinoSettings.Untrusted (such as Dictionary<long, DateTime>) no longer reads up to a key's size past the end of the input, which could crash the process when the input ended at a page boundary.
  • Rust code generated by nino-gen reads a HashSet<string> (any string set, [NinoUtf8] included) that holds null, as .NET writes it: its elements are now Option<WideString> / Option<String> / Option<Utf8>. Breaking for Rust code that builds or reads such sets (IndexSet<String> becomes IndexSet<Option<String>>).

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.8

106 commits

⚠️ Breaking changes

✨ Features

⚡ Performance

🐛 Fixes

♻️ Refactoring and cleanup (30)
📖 Documentation (36)
  • ...
Read more

v4.0.0-rc.8

v4.0.0-rc.8 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Nov 15:02

Install: dotnet add package Nino --version 4.0.0-rc.8 (Unity package: 4.0.0-preview.208)

Other languages: go get github.com/JasonXuDeveloper/Nino/runtimes/go/v4@v4.0.0-rc.8, cargo add nino-serialization@=4.0.0-rc.8, npm install nino-serialization@4.0.0-rc.8.

Wire format

Only these write different bytes than v4.0.0-rc.7:

  • On 32-bit x86, a member whose type is a struct (not a [NinoType]) holding a TimeOnly, Complex, Int128 or UInt128 after a smaller field is now written in the canonical layout, as on 64-bit runtimes, instead of its x86 memory.
  • Types with a member that is a tuple of eight items or more now write it, every item in order as for shorter tuples, where they wrote nothing.
  • T?[] and other T? sequences of a raw [NinoType] struct whose formatter the runtime resolver builds (types no generated code names, serialized by Type or boxed) are written at the fixed stride generated code uses, so either side reads the other's bytes; payloads such a runtime-bound formatter wrote before must be rewritten.
  • Unmanaged structs that are not [NinoType] and hold an Int128 or UInt128 now write their padding as zeros on .NET 8 and later, as the canonical layout requires (it was copied from memory, so it held whatever was there).
  • A closed generic [NinoType] struct (Pad<int>) held as a member or collection element is written member by member, as its formatter writes it everywhere else, unless its definition is raw; it was copied as raw memory with its padding there.
  • Every string member and element (and StringBuilder, TimeZoneInfo's id, [NinoUtf8] strings) is written with a 1, 3 or 5 byte header instead of a 4-byte one: null is 00, "" is 01, "abc" is 07 61 62 63, and a string of 64 to 32,767 chars starts with 80 and a 2-byte value. Strings up to 63 chars save 3 bytes each: 1,000 mixed short strings take 20,848 bytes instead of 23,848 ([NinoUtf8]: 21,884 instead of 27,884). Data written by earlier 4.0 prereleases is not readable.
  • The TypeScript runtime (the nino.ts that nino-gen writes) writes and reads strings with the 1, 3 or 5 byte headers .NET writes from rc.8 on; data written by earlier versions is not readable.
  • The Go runtime writes and reads strings with the 1, 3 or 5 byte headers .NET writes from rc.8 on; data written by earlier versions of the module is not readable.
  • The Rust runtime (nino-serialization) writes and reads strings with the 1, 3 or 5 byte headers .NET writes from rc.8 on; data written by earlier versions of the crate is not readable.
  • On 32-bit x86 .NET (Windows), raw structs (plain structs, and [NinoType] structs whose layout differs between runtimes) holding a DateTime at an offset the canonical layout pads are now written in the canonical layout, as on 64-bit runtimes, instead of their x86 memory.
  • On .NET Framework (64-bit and 32-bit), raw structs holding a DateTime whose fields the runtime reorders are now written in the canonical layout, as on 64-bit .NET, instead of their memory.

New

  • A Go runtime (runtimes/go, package nino) that reads and writes Nino's binary format, for code generated or written in the style of the TypeScript runtime.
  • Struct tags for the Go runtime: nino.Marshal and nino.Unmarshal read and write tagged Go structs without generated code, checked against the .NET layout a type declares with NinoLayout().
  • A Rust runtime (runtimes/rust, not yet published) reads and writes Nino's wire format, deep nesting and version-tolerant and keyed types included, with no unsafe code and no panics on any input.
  • Rust runtime: a serde feature implementing serde for Nino's value types, and FromStr for Decimal, Guid and BigInteger, so structs with Nino codecs can derive serde as well.
  • Rust runtime: #[derive(NinoRead, NinoWrite)] (feature derive) with #[nino(...)] attributes for the format, kind, type id, member order, keys and [NinoUtf8], checked against a published layout at compile time.
  • Warning NINO027 flags [NinoFormerName] on a generic type that is written with a type id: every instantiation would share the one id and collide.
  • NinoTypeLayoutAttribute.Deferrable says whether a type's bodies nested past 128 class bodies are deferred, and the layout's text form carries it as deferrable=true|false, so code generators for other languages read it instead of re-deriving .NET's rules.
  • Go and Rust read whether a type is deferrable from its published layout (deferrable=true|false); the Rust derive's deferrable attribute is now optional when layout is given.
  • Go code generation: nino-gen --lang go writes Go structs (with nino tags and json/yaml tags, camelCase by default) and codecs for an assembly's [NinoType]s, for the Go runtime in runtimes/go.
  • Rust code generation: nino-gen --lang rust writes Rust structs and codecs for an assembly's [NinoType]s, for the nino-serialization crate, with optional serde derives (--serde).
  • nino-gen (.NET tool Nino.Codegen): writes TypeScript, Go or Rust types and codecs for the [NinoType]s of a built assembly, from the layouts the generator publishes in it (nino-gen --assembly <dll> --lang ts|go|rust --out <dir>).
  • NinoDataException (namespace Nino.Core, derived from InvalidOperationException) is thrown for every invalid input Nino rejects while deserializing, so untrusted data can be caught with one specific type.
  • NinoSettings.MaxDepth (default 512, at least 128) and NinoSettings.MaxCollectionLength (default int.MaxValue) bound nesting and the element count a collection in the data may claim; set them before the first serialization or deserialization (later they throw).
  • NinoSettings.MaxHashCollisions (default 100): a key read from the data that finds more keys than this in its hash bucket throws NinoDataException, so data built to make keys collide (hash flooding) can no longer make reading a Dictionary, HashSet, lookup, ImmutableDictionary or ImmutableHashSet quadratic.
  • NinoSettings.Untrusted (off by default): for data from someone who could craft it, the dictionaries and sets Nino creates hash integer, char, Guid and string keys with a per-process secret, raw structs are checked as their constructors would (bool, DateTime, decimal, DateOnly, TimeOnly), and bytes left after the value throw NinoDataException. Off, it costs nothing.
  • Generic [NinoType]s with non-public members no longer need to be partial when the project targets .NET 9 or later: generated code reaches the members through [UnsafeAccessor], as it does for non-generic types since .NET 8.
  • Nino.Codegen writes your [NinoType]s as TypeScript, Go or Rust after each build: declare <NinoCodegen Include="typescript" Out="ts/" /> (or go, rust) with nino-gen's options as metadata. The nino-gen command is now the Nino.Codegen.Tool dotnet tool. Nino.Codegen replaces Nino.TypeScript, whose NinoTypeScript* properties still work with a warning (NINOGEN003).
  • nino-gen generates generic [NinoType]s as generic types in TypeScript, Go and Rust (Box<T>, Box[T any], BoxType<T>), with codecs built from those of their type arguments (BoxCodec(...)), and closed uses such as Box<int> as members.
  • nino-gen's --maps indexmap|std (MSBuild metadata Maps) for Rust: std writes std::collections::HashMap and HashSet instead of indexmap's, with no dependency; they are written in HashMap's iteration order. indexmap stays the default.
  • nino-gen generates closed generic types in polymorphic and object slots (a generic subtype such as Box<int> : Animal) for TypeScript, Go and Rust: the closed types the assembly's code names are read from its metadata, each a case of the slots it is assignable to, behind the id of its closed .NET name.
  • The Go, Rust and TypeScript runtimes are published with every release, with Nino's version: go get github.com/JasonXuDeveloper/Nino/runtimes/go/v4 (the module path now ends in /v4), cargo add nino-serialization (with the derive feature for #[derive(NinoRead, NinoWrite)]) and npm install nino-serialization (generate with Runtime="nino-serialization" to import it).
  • The Unity package includes optional Burst-compiled kernels (Nino.Burst): with com.unity.burst installed, arrays of tuples and structs, raw structs with padding and Latin-1 strings are moved with SSSE3 or AArch64 Neon instead of Nino's scalar netstandard paths, with the same bytes.
  • useWasm() in the TypeScript runtime: longer strings are read and written through a small embedded WebAssembly module in browsers and Deno, with the JavaScript paths kept where WebAssembly SIMD, GC or the JS String Builtins are unavailable or blocked; useWasm(false) turns it off.
  • Reference tracking (trackReferences) in the TypeScript, Go and Rust runtimes, for the code nino-gen writes; Rust holds tracked objects as nino::Shared<T>.
  • nino-gen generates generic types written behind a type id (non-sealed, abstract, interface), the closed types their slots need, Nullable and KeyValuePair type arguments inside collections, reference tracking (trackReferences) and [NinoType] collections for TypeScript, Go and Rust, and generic types holding themselves with other type arguments for TypeScript.
  • deserializeExact in the TypeScript runtime (nino-serialization): deserialize that also throws a NinoError when bytes follow the value, as Go's DeserializeExact and Rust's deserialize_exact do.
  • Nino.WebGL in the Unity package: WebGL players built with WebAssembly SIMD (Unity 6's Target WebAssembly 2023, or --compiler-flags=-msimd128 on 2022.3) move Latin-1 strings of 16 chars or more and arrays of tuples and padded raw structs of 32 or more with wasm SIMD kernels, the same bytes as without them.
  • NinoKernels is now a sealed table of native function pointers (delegate* unmanaged[Cdecl] properties Permute, Narrow, Widen and the new `...
Read more

v4.0.0-rc.7

v4.0.0-rc.7 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 23 Oct 08:18

Install: dotnet add package Nino --version 4.0.0-rc.7 (Unity package: 4.0.0-preview.207)

New

  • The Unity package includes Nino's code fixes: Rider and Visual Studio offer the same quick fixes for Nino's analyzer warnings as in a NuGet project.

Performance

  • Serializing a list or array of fixed-size values to a byte[] writes straight into an array of its size, without a copy out of the scratch buffer.
  • Without AVX-512, lists and arrays of value tuples and other padded raw values are moved with in-lane shuffles (AVX2, SSSE3) instead of cross-lane permutes or one value at a time: a list of 1,000 (int, long) serializes in 194 ns instead of 380 ns (GitHub's x64 runners, EPYC 7763, AVX2).
  • On Intel x64 CPUs, deserializing into a new list or array of padded value tuples or DateTimeOffsets is faster: the array is allocated zeroed, as stores into an uninitialized array read each cache line from memory first.
  • Copies of 33 to 128 bytes are inline vector moves instead of a memmove call, and reads of raw values into an existing array use Nino's vector copy: small raw arrays, lists and non-Latin-1 strings read faster.
  • Multi-dimensional arrays of fixed-size elements (tuples, DateTimeOffsets, raw structs) are read and written as blocks instead of one value at a time.
  • ConcurrentStack<T> and ConcurrentBag<T> of fixed-size elements serialize as one block, and PriorityQueue reads and writes and HashSet writes of non-raw elements loop on a local reader or writer.
  • Nullable members of raw types (int?, long?, double?, DateTime? ...) are read under one bounds check for the flag and the value.
  • Version and the Unity types of Nino.UnityTypes (AnimationCurve, Gradient, RectOffset) reserve their bytes at once and read each run of keys under one bounds check.
  • Serializing an ImmutableStack<T> (Nino.Immutable) allocates nothing, as it no longer builds and reverses a list: 1,000 ints in 2.7 µs instead of 4.5 µs on GitHub's Arm64 runners (Neoverse-N2) and 2.9 µs instead of 3.1 µs on x64 (EPYC 7763).
  • Serializing collections without a specialized path (Collection<T>, ObservableCollection<T>, custom ICollection<T> types) no longer stores and reloads the writer around every element.
  • Version-tolerant types read adjacent raw members as one block whenever the data holds all of them: the benchmark's array of version-tolerant classes deserializes in 49.8 µs instead of 57.3 µs (GitHub's x64 runners, EPYC 7763).
  • Serializing keyed types is a little faster: a keyed member's key and region length, and the raw members just before it, are reserved at once.
  • Lists of [NinoPooled] types deserialize a little faster: renting from the pool is inlined into their element loops.
  • Serializing arrays and lists of tuples, KeyValuePairs and other padded structs takes the same time wherever the output buffer starts, instead of slowing down at some alignments.
  • New arrays of 2 to 6 KB that a read fills are allocated zeroed, which is cheaper there than uninitialized: 1,000 ints deserialize into a ReadOnlyCollection<T> or ReadOnlySequence<T> in 257 and 264 ns instead of 348 ns (GitHub's x64 runners, EPYC 7763) and in 364 and 363 ns instead of 416 and 423 ns (Arm64 runners, Neoverse-N2), and into a Memory<T> in 262 ns instead of 312 ns (x64).
  • On Arm64, arrays and lists of padded structs (tuples, KeyValuePairs) are copied with load and store pairs, 32 bytes a step.
  • Block copies of 8 to 32 KB (4 to 16 KB without 64-byte vectors) use Nino's vector loop instead of memmove, which is several times slower on some AMD EPYC CPUs.
  • Arrays and lists of 5-byte values such as (byte, int) move several values at a time with AVX2 and SSSE3 (x64 without AVX-512), instead of one at a time.
  • HashSet<T> and Dictionary<TKey, TValue> of raw values are written from their entries in one loop when no entry has been removed, on .NET, .NET Framework, Mono and Unity.
  • On Arm64, arrays and lists of small padded tuples read and write faster: 1,000 (byte, int) deserialize in 805 ns instead of 1,123 ns and serialize in 122 ns instead of 422 ns, 1,000 (short, int) in 801 ns instead of 1,103 ns and 124 ns instead of 200 ns, and 1,000 (int, long) in 1,415 ns instead of 1,815 ns and 219 ns instead of 375 ns (GitHub's Arm64 runners).
  • Subtypes read or written through a generic base (abstract class Box<T>, IRepository<T>) take the same generated dispatch as through a non-generic base, for the subtypes of each closed base declared in its assembly, instead of a registry lookup and a virtual call for each value.
  • Subtypes of a generic base whose type argument is a reference type or a tuple holding one (Box<string>, Box<(int, string)>) read and write the base's members without a formatter lookup and a virtual call for each value.
  • On x64 CPUs without AVX-512, block copies of up to 1 MB use Nino's 32-byte vector loop instead of memmove.
  • Nino.Immutable reads ImmutableDictionary and ImmutableHashSet by building their tree directly in one pass: 1,000 ints deserialize in 20.6 and 19.7 µs instead of 109.4 and 92.1 µs (GitHub's x64 runners, EPYC 7763) and in 21.3 and 20.4 µs instead of 81.7 and 78.8 µs (Arm64 runners, Neoverse-N2).
  • Nino.Immutable reads ImmutableSortedDictionary and ImmutableSortedSet by building their tree directly in one pass: 1,000 ints deserialize in 13.3 and 13.2 µs instead of 55.6 and 18.5 µs (GitHub's x64 runners, EPYC 7763) and in 14.6 and 13.8 µs instead of 62.5 and 19.1 µs (Arm64 runners, Neoverse-N2).
  • LinkedList<T> and SortedDictionary<TKey, TValue> are read by linking their nodes or building their tree directly: 1,000 ints deserialize into a LinkedList in 11.5 µs instead of 14.1 µs (GitHub's x64 runners, EPYC 7763) and 12.3 µs instead of 14.5 µs (Arm64 runners, Neoverse-N2), and into a SortedDictionary in 17.4 µs instead of 23.9 µs and 16.7 µs instead of 27.1 µs.
  • Nino.Immutable writes ImmutableDictionary, ImmutableHashSet, ImmutableSortedDictionary and ImmutableSortedSet straight out of their trees: 1,000 ints serialize 4 to 5 times as fast for the first two and 2 to 2.6 times as fast for the sorted ones (GitHub's x64 runners, EPYC 7763, and Arm64 runners, Neoverse-N2).
  • SortedSet<T> and SortedDictionary<TKey, TValue> serialize copied out of their trees: 1,000 ints in 2.8 µs instead of 5.5 µs (SortedSet) and 3.0 to 3.2 µs instead of 5.6 to 5.8 µs (SortedDictionary) (GitHub's x64 runners, EPYC 7763, and Arm64 runners, Neoverse-N2).
  • Empty collections skip their element loops, and a pool that was never filled answers a rent without a call or a thread-local access: a pooled node deserializes in 156 ns instead of 205 ns (GitHub's x64 runners, EPYC 7763) and 145 ns instead of 193 ns (Arm64 runners, Neoverse-N2).
  • New HashSet<T> and Dictionary<TKey, TValue> of raw elements are filled directly: 1,000 ints deserialize into a HashSet in 2.6 µs instead of 6.1 µs (GitHub's x64 runners, EPYC 7763) and 2.8 µs instead of 5.3 µs (Arm64 runners, Neoverse-N2).
  • On Arm64, block copies of up to 64 KB use paired vector loads from an aligned source instead of memmove.
  • On AMD x64 CPUs, reads into a new array copy with vector stores instead of memmove, whose rep movsb is slower there: 1,000 KeyValuePair<int, int> deserialize in 418 ns instead of 465 ns (GitHub's x64 runners, EPYC 7763).
  • Arrays of polymorphic classes are faster both ways: 100 of them serialize in 34 µs instead of 46 µs and deserialize in 172 µs instead of 224 µs (GitHub's x64 runners, Xeon 6973P-C), and serialize in 58 µs instead of 76 µs and deserialize in 193 µs instead of 221 µs (Arm64 runners, Neoverse-N2).

Fixes

  • Serializing a span of unmanaged values past 4 GB, a StringBuilder past 1 G chars or UTF-8 text past 2 GB throws OverflowException instead of writing past the end of the buffer.
  • Generic abstract classes, generic interfaces and abstract classes nested in generic types with subtypes no longer break the build: their subtypes are written and read through the registry, in every format.
  • A class nested in a generic class with [NinoType] subtypes in another assembly no longer overflows the stack the first time it is serialized.
  • Code fixes (implementing [NinoPooled] hooks, adding or removing attributes) keep the line breaks of the file they edit, instead of inserting the machine's (CRLF lines in an LF file on Windows).
  • Data crafted against a reader can no longer crash the process: a struct nested in itself thousands deep (through a list, nullable or interface member) throws instead of overflowing the stack.
  • A failed read into an existing value no longer leaves an array of uninitialized memory in it, and a StringBuilder or multi-dimensional array length is checked against the data before allocating.
  • Invalid Version, Uri, CultureInfo, TimeZoneInfo and DateTimeOffset values and null dictionary keys throw Nino's InvalidOperationException (the original as its inner exception) instead of BCL exceptions, so Nino.AspNetCore answers them with 400.
  • Data that claims more elements than it holds can no longer make a read allocate hundreds of times its size: every collection allocates ahead of its elements at most 16 times the bytes left in the input, and grows as elements are actually read.

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.6

158 commits

✨ Features

⚡ Performance (65)
Read more

v4.0.0-rc.6

v4.0.0-rc.6 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 19 Oct 02:36

Install: dotnet add package Nino --version 4.0.0-rc.6 (Unity package: 4.0.0-preview.206)

Wire format

Only these write different bytes than v4.0.0-rc.5:

  • The default string encoding writes text whose chars are all at most U+00FF (ASCII, Latin-1) in one byte a char, behind a negative length header; other text stays UTF-16 and [NinoUtf8] is unchanged. This release still reads the UTF-16 strings earlier v4 releases wrote, but earlier releases cannot read the one-byte strings it writes: upgrade the readers of your data before or together with its writers.

New

  • Nino.AspNetCore: MVC input and output formatters (AddNinoFormatters(), application/x-nino) and Minimal API support (NinoBody<T> parameters, NinoResults.Ok, ReadFromNinoAsync / WriteAsNinoAsync), reading and writing bodies straight from the request and response pipes. NinoSerializer.IsSupported(Type) tells whether Nino can serialize a type.
  • [assembly: NinoTypeLayout] now describes each member by the bytes it is written as (seq(string), map(string,seq(raw(4))), nullable(raw(4)), type(MyType) ...) instead of the formatter class, and keyed members carry their key (:key=0x…).
  • Nino.TypeScript, a build package that writes TypeScript classes, enums and codecs for the project's [NinoType]s after each build (NinoTypeScriptOutput, NinoTypeScriptImportExtension, NinoTypeScriptNames), with the nino.ts runtime, so browsers and Node read and write exactly the bytes Nino writes in .NET.

Performance

  • Payloads with Latin-1 strings are smaller, with the same serialize and deserialize times as before: the benchmark's Orders 30,848 → 26,126 bytes, Players 45,124 → 39,164 and Tree 40,676 → 31,258, now smaller than MemoryPack's (27,990, 43,364 and 32,623).
  • TypeScript serializers are faster: 1,000 player records serialize in 1.15 ms instead of 2.07 ms and deserialize in 1.17 ms instead of 1.92 ms (Node), text-heavy records 6.8x and 11x faster, and arrays of numbers 2-5x faster.
  • TypeScript deserializes text faster: lists of small records with short strings in 0.46 ms instead of 0.81 ms in Node (0.42 ms instead of 0.65 ms in Chromium), maps of short strings 1.5x faster in Node; in Chromium, arrays of numbers and structs serialize 1.3-1.5x faster.
  • Lists and arrays of value tuples, KeyValuePairs and other raw values with padding are written as whole, aligned vectors of their bytes: serializing a List<(int, long)> of 1,000 values takes 106 to 109 ns instead of 228 ns in rc.5 on an AVX-512 Xeon (MemoryPack: 136 to 141 ns), and AVX-512 CPUs without VBMI use the grouped path too.
  • Reading arrays, lists and read-only collections of 2 to 6 KB of unmanaged values allocates them without zeroing: a ReadOnlyCollection<int> of 1,000 deserializes in 450 to 510 ns instead of 570 to 590 ns.
  • Deserializing 1,000 (int, long) into an existing list or array on an AVX-512 Xeon takes 105 to 140 ns instead of 255 ns in rc.5, on par with MemoryPack (111 to 130 ns), which copies its 16-byte values as they are; 1,000-char non-Latin-1 strings deserialize about 8% faster.

Fixes

  • TypeScript: a [NinoUtf8] string starting with U+FEFF no longer fails to deserialize.
  • Unity projects that reference a library with an internal ModuleInitializerAttribute polyfill (such as ZLinq's netstandard build) compile again: the generated registry no longer names that attribute (error CS0122).

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.5

26 commits

✨ Features

⚡ Performance

🐛 Fixes

📖 Documentation

🧪 Tests

🔧 Build, CI and dependencies

Contributors

Thanks to everyone who contributed to this release: @JasonXuDeveloper

Full Changelog: v4.0.0-rc.5...v4.0.0-rc.6

📊 v3 3.3.13 vs v4 (same machine)

Nino v3 3.3.13 (NuGet) against v4.0.0-rc.6, back to back on the same runner

benchmarks/v3 (the Nino and MemoryPack cases of the same SimpleTest, built against Nino 3.3.13 from nuget.org) ran directly before the v4
SimpleTest run below, on the same runner, with the same BenchmarkDotNet settings
(--launchCount 3 --warmupCount 5 --iterationCount 15). MemoryPack runs in both, so its two columns show how much the runner moved.

v4 vs v3

case v3 3.3.13 v4 v4 vs v3 MemoryPack (v4 run) MemoryPack (v3 run)
SerializeSimpleClass 185.92 ns 101.90 ns 0.55x 395.72 ns 442.09 ns
SerializeSimpleClasses 25.35 μs 11.75 μs 0.46x 39.57 μs 39.87 μs
SerializeSimpleStruct 3.62 ns 4.54 ns 1.25x 5.14 ns 5.13 ns
SerializeSimpleStructs 22.49 ns 21.93 ns 0.98x 22.53 ns 21.95 ns
SerializeVectors 3.31 μs 3.31 μs 1.00x 3.44 μs 3.31 μs
SerializeFixedClass - 10.29 ns - 24.92 ns -
SerializeFixedClasses - 747.63 ns - 1.74 μs -
SerializeFixedStruct - 9.36 ns - 9.19 ns -
SerializeInventory - 90.72 ns - 93.18 ns -
SerializeOrders - 9.71 μs - 15.50 μs -
SerializePlayers - 10.62 μs - 26.04 μs -
SerializeShapes - 1.84 μs - 22.94 μs -
SerializeStringDictionary - 8.61 μs - 32.70 μs -
SerializeTree - 15.90 μs - 20.56 μs -
SerializeBigInteger - 13.56 ns - - -
SerializeBitArray - 25.62 ns - - -
SerializeBlockingCollection - 5.52 μs - 11.61 μs -
SerializeCultureInfo - 5.59 ns - 27.58 ns -
SerializeImmutableDictionary - 19.08 μs - 20.36 μs -
SerializeImmutableHashSet - 15.26 μs - 22.32 μs -
SerializeImmutableQueue - 1.98 μs - 10.34 μs -
SerializeImmutableSortedDictionary - 8.61 μs - 9.66 μs -
SerializeImmutableSortedSet - 7.96 μs - 13.80 μs -
SerializeImmutableStack ...
Read more

v4.0.0-rc.5

v4.0.0-rc.5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Oct 12:16

Install: dotnet add package Nino --version 4.0.0-rc.5 (Unity package: 4.0.0-preview.205)

New

  • Built-in formatters for BigInteger, Uri, Version, StringBuilder, BitArray, CultureInfo, TimeZoneInfo, Lazy<T>, Memory<T>, ReadOnlyMemory<T>, ReadOnlySequence<T>, ReadOnlyCollection<T>, ReadOnlyObservableCollection<T>, ILookup<TKey, TElement>, IGrouping<TKey, TElement> and BlockingCollection<T>; Nino.Immutable adds ImmutableHashSet, ImmutableSortedSet, ImmutableQueue, ImmutableStack, ImmutableDictionary and ImmutableSortedDictionary.
  • Unity's AnimationCurve, Gradient and RectOffset serialize out of the box, as members, elements and top-level values, null and ref reuse included: the Unity package ships Nino.UnityTypes.dll with their formatters.

Performance

  • Stack<T>, Queue<T> and collections declared as interfaces (IList<T>, IEnumerable<T>, ...) of unmanaged values or raw [NinoType] structs are written and read as one block: with 1,000 ints, 17× to 75× faster to serialize and 2.5× to 6× faster to deserialize. The bytes are unchanged.
  • Arrays and lists of [NinoType] structs keep the generated formatter after the struct was serialized on its own: InventoryItem[1000] (int, short, bool) serializes in 0.11 µs instead of 1.7 µs and deserializes in 0.7 µs instead of 2.6 µs. Short strings and small blocks are copied inline: List<KeyValuePair<int, string>> serializes in 3.6 µs instead of 4.7 µs.
  • ConcurrentDictionary<,> deserializes 28× faster (1,000 int pairs: 66 µs instead of 1.88 ms), and SortedDictionary<,> 1.8× faster (53 µs instead of 96 µs), on par with MemoryPack.
  • Reading strings is faster on .NET 8 and later: a string of 3 to 12 chars takes 12 to 14 ns instead of 16 to 19, so types with many short strings deserialize 3 to 7% faster (string dictionaries 7%, trees of named nodes 5%).
  • Serializing and deserializing classes and structs with nested members is faster: a list of players serializes 19% faster and deserializes 8% faster, lists of small classes 5 to 8%, orders 6% and trees 6 to 10%.
  • Reading a new List of unmanaged values or raw structs of 6 KB or more no longer clears its backing array first: a list of 10,000 ints deserializes about 15% faster (4.7 to 4.0 µs).
  • ImmutableArray and ImmutableList (Nino.Immutable) are much faster: an ImmutableArray of 1,000 serializes 13× faster (1,075 to 83 ns) and deserializes 8× faster (2.7 to 0.34 µs), ImmutableArray deserializes 11% faster, and an ImmutableList of 1,000 deserializes 3.6× faster (62 to 17.5 µs) and serializes 7% faster.
  • Arrays and lists of value tuples and KeyValuePairs of unmanaged values are written and read as one block: 1,000 (int, int) or KeyValuePair<int, int> serialize 16× to 19× faster (1.1-1.3 µs to 68 ns, MemoryPack 93 ns) and deserialize 2.5× to 3× faster; a List<(int, long)> of 1,000 serializes 5.4× faster (1,246 to 231 ns) and deserializes 2× faster.
  • Deserializing a ConcurrentDictionary<TKey, TValue> is about 4x faster (1,000 int pairs: 65 µs to 15 µs, 4.2x MemoryPack's time).
  • Deserializing a new ConcurrentStack<T> is about 1.9x faster (1,000 ints: 17.9 µs to 9.3 µs, half of MemoryPack's time).
  • DateTimeOffset is read and written through its two fields, and arrays and lists of it as one block: with 1,000 values, 2.9x faster to serialize and 2x faster to deserialize. The bytes are unchanged.
  • The body of a small class (at most eight members, at most two of them needing a formatter call) is inlined into collection loops, also when the class is not sealed: a list of such a class serializes about 2x faster and deserializes about 1.3x faster.
  • Blocks of 64 bytes to 8 KB are written with 64-byte vectors on CPUs with AVX-512 (up to 1.5x faster), and arrays of padded structs with a 64-byte masked copy: 1,000 structs of int, short and bool serialize 1.6x faster.
  • Structs with a string or another non-raw member are passed to their generated body by reference: a list of 1,000 structs of four longs, a string and an int serializes 2x faster.
  • SortedSet, SortedDictionary, SortedList, LinkedList and ConcurrentDictionary are read and written through their own fields and methods: with 1,000 elements, a SortedList deserializes 5.5x faster and a SortedSet serializes and deserializes 2x faster.
  • On x64 CPUs without AVX-512, blocks of 256 bytes to 4 KB are copied with 32-byte vectors (2,500 bytes in 24.6 ns instead of 36.3); on Arm64, arrays of value tuples, KeyValuePairs and raw structs of up to 8 bytes are rearranged 64 bytes at a time, and on .NET 10 the 16-byte shuffle runs with WebAssembly SIMD too.

Fixes

  • On .NET Framework and Mono, List<T> was written empty and a list read into was left broken: the layout of each collection whose fields Nino uses is now checked once at startup, with a fallback to its public methods.

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.4

37 commits

✨ Features

📦 Runtime, generator and analyzer

⚡ Performance

🐛 Fixes

🧪 Tests

🔧 Build, CI and dependencies

Contributors

Thanks to everyone who contributed to this release: @JasonXuDeveloper

Full Changelog: v4.0.0-rc.4...v4.0.0-rc.5

📊 v3 3.3.13 vs v4 (same machine)

Nino v3 3.3.13 (NuGet) against v4.0.0-rc.5, back to back on the same runner

benchmarks/v3 (the Nino and MemoryPack cases of the same SimpleTest, built against Nino 3.3.13 from nuget.org) ran directly before the v4
SimpleTest run below, on the same runner, with the same BenchmarkDotNet settings
(--launchCount 1 --warmupCount 5 --iterationCount 15). MemoryPack runs in both, so its two columns show how much the runner moved.

v4 vs v3

| case | v3 3.3.13 | v4 | v4 vs v3 | MemoryPack (v4 run) | MemoryPack (v3 run) |
|--------------------------------------|----------:|----------:|---------:|------------...

Read more

v4.0.0-rc.4

v4.0.0-rc.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 14:47

Install: dotnet add package Nino --version 4.0.0-rc.4 (Unity package: 4.0.0-preview.204)

New

  • NinoSerializer.Serialize<T>(in T value, IBufferWriter<byte>), Serialize<T>(in T value, ref Writer) and Writer.WriteValue<T>(in T) take the value by reference too, so a struct read from a field is written without a copy on the stack. Source compatible.
  • Writer.Write<T>(in T value) takes an unmanaged value by reference, as the Serialize overloads do. Source compatible.

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.3

3 commits

📦 Runtime, generator and analyzer

📖 Documentation

Contributors

Thanks to everyone who contributed to this release: @JasonXuDeveloper

Full Changelog: v4.0.0-rc.3...v4.0.0-rc.4

📊 v3 3.3.13 vs v4 (same machine)

Nino v3 3.3.13 (NuGet) against v4.0.0-rc.4, back to back on the same runner

benchmarks/v3 (the same SimpleTest, built against Nino 3.3.13 from nuget.org) ran directly before the v4
run below, in the same job on the same runner, with the same BenchmarkDotNet settings
(--launchCount 1 --warmupCount 5 --iterationCount 15). MemoryPack runs in both, so its two columns show how much the runner moved.

v4 vs v3

case v3 3.3.13 v4 v4 vs v3 MemoryPack (v4 run) MemoryPack (v3 run)
SerializeFixedClass - 9.93 ns - 18.51 ns -
SerializeFixedClasses - 634.26 ns - 1.43 μs -
SerializeFixedStruct - 8.98 ns - 12.99 ns -
SerializeGraph - 4.47 μs - 23.34 μs -
SerializePolymorphicClassArray - 59.47 μs - 229.29 μs -
SerializePolymorphicClass - 6.62 μs - 21.31 μs -
SerializePolymorphicFooArray - 5.69 μs - 21.51 μs -
SerializeSimpleClass 132.78 ns 87.71 ns 0.66x 296.42 ns 340.41 ns
SerializeSimpleClasses 19.29 μs 9.97 μs 0.52x 31.37 μs 31.03 μs
SerializeSimpleStruct 1.46 ns 0.82 ns 0.56x 2.78 ns 2.80 ns
SerializeSimpleStructs 13.65 ns 14.17 ns 1.04x 14.64 ns 21.37 ns
SerializeTolerantClass - 85.92 ns - 375.76 ns -
SerializeTolerantClasses - 9.64 μs - 39.17 μs -
SerializeVectors 3.90 μs 3.90 μs 1.00x 3.91 μs 3.90 μs
DeserializeFixedClass - 7.87 ns - 18.59 ns -
DeserializeFixedClasses - 1.04 μs - 1.16 μs -
DeserializeFixedStruct - 0.04 ns - 0.39 ns -
DeserializeGraph - 5.16 μs - 16.53 μs -
LeavesNew - 3.29 μs - 3.33 μs -
LeavesPooledElements - 2.27 μs - - -
LeavesInPlace - 467.56 ns - - -
NodeNew - 535.74 ns - 548.04 ns -
NodePooled - 185.17 ns - - -
PlainNew - 2.53 μs - 3.36 μs -
DeserializePolymorphicClassArray - 265.10 μs - 347.96 μs -
DeserializePolymorphicClass - 17.63 μs - 24.85 μs -
DeserializePolymorphicFooArray - 16.73 μs - 24.39 μs -
DeserializeNew - 446.99 ns - 577.79 ns -
DeserializePooled - 219.97 ns - - -
DeserializeSimpleClass 473.86 ns 444.33 ns 0.94x 576.74 ns 574.68 ns
DeserializeSimpleClasses 49.74 μs 45.41 μs 0.91x 57.24 μs 57.18 μs
DeserializeSimpleStruct 0.41 ns 0.39 ns 0.95x 0.39 ns 0.34 ns
DeserializeSimpleStructs 86.34 ns 86.60 ns 1.00x 97.57 ns 97.67 ns
DeserializeTolerantClass - 431.29 ns - 629.27 ns -
DeserializeTolerantClasses - 46.04 μs - 64.36 μs -
DeserializeVectors 102.72 μs 106.24 μs 1.03x 109.03 μs 103.44 μs

v4 vs v3 = mean(v4) / mean(v3); below 1.00x means v4 is faster.
MemoryPack drift (v4 run / v3 run): min 0.69x, max 1.13x (same code in both runs; the spread is the noise floor of this comparison).

v3 3.3.13 report


BenchmarkDotNet v0.15.8, Linux Ubuntu 26.04.1 LTS (Resolute Raccoon)
INTEL XEON PLATINUM 8573C 2.96GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.402
  [Host]             : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v4
  ShortRun-.NET 10.0 : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v4

Job=ShortRun-.NET 10.0  Runtime=.NET 10.0  Server=True  
IterationCount=15  LaunchCount=1  WarmupCount=5  

Method Mean Min Max Ratio Payload Gen0 Gen1 Gen2 Allocated Alloc Ratio
NinoDeserializeSimpleClass 473.8551 ns 472.5965 ns 477.1593 ns 1.00 - 0.2289 - - 2552 B 1.00
MemoryPackDeserializeSimpleClass 574.6807 ns 572.1169 ns 576.7391 ns 1.21 - 0.2413 - - 2528 B 0.99
MessagePackDeserializeSimpleClass 1,559.2117 ns 1,534.8606 ns 1,586.1142 ns 3.29 - 0.4215 0.0038 0.0019 - 0.00
NinoSerializeSimpleClass 132.7801 ns 132.1466 ns 133.2328 ns 1.00 1.13KB - - - - NA
MemoryPackSerializeSimpleClass 340.4064 ns 339.8059 ns 341.7636 ns 2.56 1.12KB - - - - NA
MessagePackSerializeSimpleClass 770.7891 ns 768.8618 ns 773.2303 ns 5.81 1.09KB - - - - NA
NinoDeserializeSimpleClasses 49,738.7632 ns 49,114.9208 ns 50,121.0950 ns 1.00 - 18.6768 5.7373 - 262424 B 1.00
MemoryPackDeserializeSimpleClasses 57,178.4277 ns 56,624.4812 ns 57,770.5761 ns 1.15 - 18.0054 5.5542 - 253624 B 0.97
MessagePackDeserializeSimpleClasses 169,045.1661 ns 168,474.3135 ns 169,701.1729 ns 3.40 - 18.0664 5.6152 - 253624 B 0.97
NinoSerializeSimpleClasses 19,288.8669 ns 19,098.7635 ns 19,570.2582 ns 1.00 112.5KB - - - - NA
MemoryPackSerializeSimpleClasses 31,029.8530 ns 30,922.3727 ns 31,120.7873 ns 1.61 112.11KB - - - - NA
MessagePackSerializeSimpleClasses 75,124.6973 ns 74,330.2264 ns 76,406.2806 ns 3.89 108.59KB - - - - NA
NinoDeserializeSimpleStruct 0.4064 ns 0.4036 ns 0.4108 ns 1.00 - - - - - NA
MemoryPackDeserializeSimpleStruct 0.3449 ns 0.3430...
Read more

v4.0.0-rc.3

v4.0.0-rc.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 13:34

Install: dotnet add package Nino --version 4.0.0-rc.3 (Unity package: 4.0.0-preview.203)

Wire format

Only these write different bytes than v4.0.0-rc.2:

  • A [NinoType] struct with [StructLayout(LayoutKind.Explicit)] from another assembly is written member by member in every build, as it already was when seen as source. From a package or a command-line build, rc.2 copied it as raw memory (#202).

New

  • [assembly: NinoTypeLayout] shows the bytes of every value copied as raw memory: its size and each field's offset, also inside structs that are not [NinoType]s, so changing a field, Pack, Size or [FieldOffset] changes the layout. Raw members now read raw(n){name:type@offset:encoding,...}, and the members of a raw type are its raw encoding (#202). Layout Metadata
  • NinoSerializer.Serialize<T>(in T value) takes the value by reference, so serializing a large struct read from a field no longer copies it to the stack first. Source compatible; a method group converted to Func<T, byte[]> needs a lambda instead.

Fixes

  • The generator reads [StructLayout] and [FieldOffset] of structs from another assembly, which Roslyn does not show as attributes. A struct with explicit field offsets from a package could be copied as raw memory, whose size differs between 64-bit and 32-bit runtimes (#202).

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.2

6 commits

📦 Runtime, generator and analyzer

🔧 Build, CI and dependencies

Contributors

Thanks to everyone who contributed to this release: @JasonXuDeveloper

Full Changelog: v4.0.0-rc.2...v4.0.0-rc.3

📊 v3 3.3.13 vs v4 (same machine)

Nino v3 3.3.13 (NuGet) against v4.0.0-rc.3, back to back on the same runner

benchmarks/v3 (the same SimpleTest, built against Nino 3.3.13 from nuget.org) ran directly before the v4
run below, in the same job on the same runner, with the same BenchmarkDotNet settings
(--launchCount 1 --warmupCount 5 --iterationCount 15). MemoryPack runs in both, so its two columns show how much the runner moved.

v4 vs v3

case v3 3.3.13 v4 v4 vs v3 MemoryPack (v4 run) MemoryPack (v3 run)
SerializeFixedClass - 11.44 ns - 26.99 ns -
SerializeFixedClasses - 825.19 ns - 1.92 μs -
SerializeFixedStruct - 9.86 ns - 10.53 ns -
SerializeGraph - 5.63 μs - 30.77 μs -
SerializePolymorphicClassArray - 76.71 μs - 347.94 μs -
SerializePolymorphicClass - 6.56 μs - 38.11 μs -
SerializePolymorphicFooArray - 6.66 μs - 32.54 μs -
SerializeSimpleClass 180.80 ns 106.89 ns 0.59x 385.68 ns 395.15 ns
SerializeSimpleClasses 23.58 μs 12.47 μs 0.53x 38.58 μs 56.75 μs
SerializeSimpleStruct 3.51 ns 4.71 ns 1.34x 4.93 ns 5.22 ns
SerializeSimpleStructs 22.71 ns 18.18 ns 0.80x 21.67 ns 21.99 ns
SerializeTolerantClass - 129.70 ns - 506.90 ns -
SerializeTolerantClasses - 12.46 μs - 51.39 μs -
SerializeVectors 3.40 μs 3.31 μs 0.97x 3.29 μs 3.20 μs
DeserializeFixedClass - 10.27 ns - 26.25 ns -
DeserializeFixedClasses - 1.25 μs - 1.39 μs -
DeserializeFixedStruct - 0.29 ns - 1.25 ns -
DeserializeGraph - 7.00 μs - 20.90 μs -
LeavesNew - 3.42 μs - 3.87 μs -
LeavesPooledElements - 3.09 μs - - -
LeavesInPlace - 491.51 ns - - -
NodeNew - 577.54 ns - 669.59 ns -
NodePooled - 225.00 ns - - -
PlainNew - 2.97 μs - 4.01 μs -
DeserializePolymorphicClassArray - 259.05 μs - 386.73 μs -
DeserializePolymorphicClass - 16.95 μs - 29.64 μs -
DeserializePolymorphicFooArray - 15.27 μs - 28.36 μs -
DeserializeNew - 515.95 ns - 747.52 ns -
DeserializePooled - 285.05 ns - - -
DeserializeSimpleClass 539.17 ns 524.91 ns 0.97x 791.29 ns 791.43 ns
DeserializeSimpleClasses 62.95 μs 54.97 μs 0.87x 75.47 μs 75.03 μs
DeserializeSimpleStruct 0.77 ns 0.72 ns 0.93x 1.46 ns 1.47 ns
DeserializeSimpleStructs 87.55 ns 86.91 ns 0.99x 98.44 ns 99.63 ns
DeserializeTolerantClass - 484.16 ns - 757.28 ns -
DeserializeTolerantClasses - 50.13 μs - 81.64 μs -
DeserializeVectors 125.09 μs 123.03 μs 0.98x 118.64 μs 121.72 μs

v4 vs v3 = mean(v4) / mean(v3); below 1.00x means v4 is faster.
MemoryPack drift (v4 run / v3 run): min 0.68x, max 1.03x (same code in both runs; the spread is the noise floor of this comparison).

v3 3.3.13 report


BenchmarkDotNet v0.15.8, Linux Ubuntu 26.04.1 LTS (Resolute Raccoon)
AMD EPYC 7763 2.59GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.402
  [Host]             : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v3
  ShortRun-.NET 10.0 : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v3

Job=ShortRun-.NET 10.0  Runtime=.NET 10.0  Server=True  
IterationCount=15  LaunchCount=1  WarmupCount=5  

Method Mean Min Max Ratio Payload Gen0 Gen1 Gen2 Allocated Alloc Ratio
NinoDeserializeSimpleClass 539.1745 ns 525.8775 ns 557.6440 ns 1.00 - 0.2232 - - 2552 B 1.00
MemoryPackDeserializeSimpleClass 791.4254 ns 782.5133 ns 803.6820 ns 1.47 - 0.2460 - - 2528 B 0.99
MessagePackDeserializeSimpleClass 1,818.5518 ns 1,802.6458 ns 1,834.3951 ns 3.37 - 0.3319 0.0019 - 2528 B 0.99
NinoSerializeSimpleClass 180.7997 ns 180.4435 ns 181.2094 ns 1.00 1.13KB - - - - NA
MemoryPackSerializeSimpleClass 395.1499 ns 394.4798 ns 396.9213 ns 2.19 1.12KB - - - - NA
MessagePackSerializeSimpleClass 1,158.8586 ns 1,149.5939 ns 1,182.8563 ns 6.41 1.09KB - - - - NA
NinoDeserializeSimpleClasses 62,954.8765 ns 60,595.9494 ns 65,918.5325 ns 1.00 - 18.5547 5.8594 - 262424 B 1.00
MemoryPackDeserializeSimpleClasses 75,029.7935 ns 74,049.4639 ns 76,177.6270 ns 1.19 - 17.5781 5.6152 - 253624 B 0.97
Messag...
Read more

v4.0.0-rc.2

v4.0.0-rc.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 11:23

Install: dotnet add package Nino --version 4.0.0-rc.2 (Unity package: 4.0.0-preview.202)

Wire format

Only these types write different bytes than with rc.1:

  • A [NinoType] struct of another assembly whose members are auto-properties (a positional record struct, for
    example) is now copied as raw memory in every build. rc.1 did so when the struct was seen as source (the IDE, a
    project reference), but wrote it member by member from a package or a command-line build. A struct, array or list
    holding such a struct can therefore differ from an rc.1 package or command-line build where the struct has padding
    (#200).
  • nint and nuint members are serialized on netstandard (Unity included), as 8 bytes. rc.1 skipped them there with
    NINO007.

New

  • [assembly: NinoTypeLayout] describes how each [NinoType] is written, for compatibility checks between builds
    (#197). Layout Metadata
  • NinoRegistry.Remove(assembly) clears what Nino cached for an assembly, so a collectible AssemblyLoadContext can
    unload it; on .NET 8 and later this runs when the context unloads (#199).
    Unloading Assemblies
  • new NinoArrayBufferWriter(initialCapacity, pool) rents from your ArrayPool<byte> (#198).

Fixes

  • Two [NinoFormerName] on one type are reported as error NINO026; the generator took the first and
    NinoTypeId.Of threw AmbiguousMatchException (#201).

Every change between v4 releases: Changelog.

What's Changed since v4.0.0-rc.1

18 commits

📦 Runtime, generator and analyzer

🐛 Fixes

♻️ Refactoring and cleanup

📖 Documentation

🧪 Tests

🔧 Build, CI and dependencies

Other changes

Contributors

Thanks to everyone who contributed to this release: @JasonXuDeveloper

Full Changelog: v4.0.0-rc.1...v4.0.0-rc.2

📊 v3 3.3.13 vs v4 (same machine)

Nino v3 3.3.13 (NuGet) against v4.0.0-rc.2, back to back on the same runner

benchmarks/v3 (the same SimpleTest, built against Nino 3.3.13 from nuget.org) ran directly before the v4
run below, in the same job on the same runner, with the same BenchmarkDotNet settings
(--launchCount 1 --warmupCount 5 --iterationCount 15). MemoryPack runs in both, so its two columns show how much the runner moved.

v4 vs v3

case v3 3.3.13 v4 v4 vs v3 MemoryPack (v4 run) MemoryPack (v3 run)
SerializeFixedClass - 8.95 ns - 17.26 ns -
SerializeFixedClasses - 562.88 ns - 1.30 μs -
SerializeFixedStruct - 8.67 ns - 7.05 ns -
SerializeGraph - 4.01 μs - 20.87 μs -
SerializePolymorphicClassArray - 54.26 μs - 206.14 μs -
SerializePolymorphicClass - 5.57 μs - 19.06 μs -
SerializePolymorphicFooArray - 5.30 μs - 18.87 μs -
SerializeSimpleClass 121.08 ns 79.77 ns 0.66x 262.33 ns 262.17 ns
SerializeSimpleClasses 17.40 μs 9.13 μs 0.52x 27.92 μs 27.92 μs
SerializeSimpleStruct 1.28 ns 1.11 ns 0.87x 2.48 ns 2.50 ns
SerializeSimpleStructs 12.28 ns 12.50 ns 1.02x 13.17 ns 14.83 ns
SerializeTolerantClass - 76.76 ns - 333.31 ns -
SerializeTolerantClasses - 8.47 μs - 34.23 μs -
SerializeVectors 3.53 μs 3.50 μs 0.99x 3.49 μs 3.54 μs
DeserializeFixedClass - 7.96 ns - 18.04 ns -
DeserializeFixedClasses - 928.57 ns - 1.04 μs -
DeserializeFixedStruct - 0.31 ns - 0.34 ns -
DeserializeGraph - 4.67 μs - 14.82 μs -
LeavesNew - 2.85 μs - 2.83 μs -
LeavesPooledElements - 2.07 μs - - -
LeavesInPlace - 462.52 ns - - -
NodeNew - 481.05 ns - 490.56 ns -
NodePooled - 171.12 ns - - -
PlainNew - 2.23 μs - 3.03 μs -
DeserializePolymorphicClassArray - 229.91 μs - 304.14 μs -
DeserializePolymorphicClass - 15.99 μs - 22.52 μs -
DeserializePolymorphicFooArray - 14.72 μs - 21.06 μs -
DeserializeNew - 418.04 ns - 520.93 ns -
DeserializePooled - 198.71 ns - - -
DeserializeSimpleClass 414.06 ns 387.21 ns 0.94x 523.76 ns 533.20 ns
DeserializeSimpleClasses 44.05 μs 41.51 μs 0.94x 52.21 μs 51.64 μs
DeserializeSimpleStruct 0.35 ns 0.00 ns 0.01x 0.32 ns 0.33 ns
DeserializeSimpleStructs 79.89 ns 78.58 ns 0.98x 89.00 ns 88.63 ns
DeserializeTolerantClass - 393.59 ns - 569.93 ns -
DeserializeTolerantClasses - 40.06 μs - 56.37 μs -
DeserializeVectors 91.62 μs 92.97 μs 1.01x 94.23 μs 91.63 μs

v4 vs v3 = mean(v4) / mean(v3); below 1.00x means v4 is faster.
MemoryPack drift (v4 run / v3 run): min 0.89x, max 1.03x (same code in both runs; the spread is the noise floor of this comparison).

v3 3.3.13 report


BenchmarkDotNet v0.15.8, Linux Ubuntu 26.04.1 LTS (Resolute Raccoon)
INTEL XEON PLATINUM 8573C 3.31GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.402
  [Host]             : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v4
  ShortRun-.NET 10.0 : .NET 10.0.13 (10.0.13, 10.0.1326.50504), X64 RyuJIT x86-64-v4

Job=ShortRun-.NET 10.0  Runtime=.NET 10.0  Server=True  
IterationCount=15  LaunchCount=1  WarmupCount=5  

Method Mean Min Max Ratio Payload Gen0 Gen1 Gen2 Allocated Alloc Ratio
NinoDeserializeSimpleClass 414.0598 ns 404.8757 ns 428.2833 ns 1.00 - 0.2260 - - 2552 B 1.00
MemoryPackDeserializeSimpleClass 533.2007 ns 513.5039 ns 559.7653 ns 1.29 - 0.2422 - - 2528 B 0.99
MessagePackDeserializeSimpleClass 1,424.7749 ns 1,391.5295 ns 1,456.2514 ns 3.44 - 0.3357 0.0038 0.0038 - 0.00
...
Read more

v4.0.0-rc.1

v4.0.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 09:21

Install: dotnet add package Nino --version 4.0.0-rc.1 (Unity package: 4.0.0-preview.201)

What's Changed

New Contributors

Full Changelog: v4.0.0-beta.47...v4.0.0-rc.1

📊 v3 3.3.13 vs v4 (same machine)

Nino v3 3.3.13 (NuGet) against v4.0.0-rc.1, back to back on the same runner

benchmarks/v3 (the same SimpleTest, built against Nino 3.3.13 from nuget.org) ran before and after the v4
run below, in the same job on the same runner, with the same BenchmarkDotNet settings
(--launchCount 1 --warmupCount 5 --iterationCount 15). MemoryPack runs in both, so its two columns show how much the runner moved.

v4 vs v3 (v3 run before v4)

case v3 3.3.13 v4 v4 vs v3 MemoryPack (v4 run) MemoryPack (v3 run)
SerializeFixedClass - 8.59 ns - 15.72 ns -
SerializeFixedClasses - 581.99 ns - 1.24 μs -
SerializeFixedStruct - 8.31 ns - 7.70 ns -
SerializeGraph - 3.91 μs - 19.73 μs -
SerializePolymorphicClassArray - 52.41 μs - 195.42 μs -
SerializePolymorphicClass - 5.36 μs - 18.42 μs -
SerializePolymorphicFooArray - 5.28 μs - 19.30 μs -
SerializeSimpleClass 112.51 ns 74.90 ns 0.67x 255.62 ns 255.01 ns
SerializeSimpleClasses 16.72 μs 8.54 μs 0.51x 26.77 μs 26.58 μs
SerializeSimpleStruct 1.21 ns 1.13 ns 0.93x 2.39 ns 2.41 ns
SerializeSimpleStructs 11.64 ns 13.95 ns 1.20x 15.92 ns 17.75 ns
SerializeTolerantClass - 73.53 ns - 319.19 ns -
SerializeTolerantClasses - 8.08 μs - 32.55 μs -
SerializeVectors 3.36 μs 3.35 μs 1.00x 3.36 μs 3.37 μs
DeserializeFixedClass - 7.61 ns - 17.67 ns -
DeserializeFixedClasses - 901.37 ns - 954.27 ns -
DeserializeFixedStruct - 0.33 ns - 0.45 ns -
DeserializeGraph - 4.37 μs - 14.33 μs -
LeavesNew - 2.84 μs - 2.80 μs -
LeavesPooledElements - 2.00 μs - - -
LeavesInPlace - 404.04 ns - - -
NodeNew - 456.01 ns - 468.28 ns -
NodePooled - 159.17 ns - - -
PlainNew - 2.20 μs - 2.76 μs -
DeserializePolymorphicClassArray - 216.65 μs - 295.36 μs -
DeserializePolymorphicClass - 15.13 μs - 21.71 μs -
DeserializePolymorphicFooArray - 14.00 μs - 20.73 μs -
DeserializeNew - 375.36 ns - 490.57 ns -
DeserializePooled - 189.82 ns - - -
DeserializeSimpleClass 401.52 ns 380.76 ns 0.95x 513.58 ns 495.49 ns
DeserializeSimpleClasses 43.81 μs 39.64 μs 0.90x 49.53 μs 49.34 μs
DeserializeSimpleStruct 0.34 ns 0.33 ns 0.97x 0.33 ns 0.29 ns
DeserializeSimpleStructs 76.94 ns 74.39 ns 0.97x 85.41 ns 85.71 ns
DeserializeTolerantClass - 377.48 ns - 552.40 ns -
DeserializeTolerantClasses - 39.97 μs - 55.10 μs -
DeserializeVectors 89.09 μs 90.55 μs 1.02x 89.59 μs 89.15 μs

v4 vs v3 = mean(v4) / mean(v3); below 1.00x means v4 is faster.
MemoryPack drift (v4 run / v3 run): min 0.90x, max 1.13x (same code in both runs; the spread is the noise floor of this comparison).

v4 vs v3 (v3 run after v4)

case v3 3.3.13 v4 v4 vs v3 MemoryPack (v4 run) MemoryPack (v3 run)
SerializeFixedClass - 8.59 ns - 15.72 ns -
SerializeFixedClasses - 581.99 ns - 1.24 μs -
SerializeFixedStruct - 8.31 ns - 7.70 ns -
SerializeGraph - 3.91 μs - 19.73 μs -
SerializePolymorphicClassArray - 52.41 μs - 195.42 μs -
SerializePolymorphicClass - 5.36 μs - 18.42 μs -
SerializePolymorphicFooArray - 5.28 μs - 19.30 μs -
SerializeSimpleClass 112.33 ns 74.90 ns 0.67x 255.62 ns 249.27 ns
SerializeSimpleClasses 16.93 μs 8.54 μs 0.50x 26.77 μs 26.58 μs
SerializeSimpleStruct 1.29 ns 1.13 ns 0.88x 2.39 ns 2.42 ns
SerializeSimpleStructs 11.75 ns 13.95 ns 1.19x 15.92 ns 14.28 ns
SerializeTolerantClass - 73.53 ns - 319.19 ns -
SerializeTolerantClasses - 8.08 μs - 32.55 μs -
SerializeVectors 3.36 μs 3.35 μs 1.00x 3.36 μs 3.35 μs
DeserializeFixedClass - 7.61 ns - 17.67 ns -
DeserializeFixedClasses - 901.37 ns - 954.27 ns -
DeserializeFixedStruct - 0.33 ns - 0.45 ns -
DeserializeGraph - 4.37 μs - 14.33 μs -
LeavesNew - 2.84 μs - 2.80 μs -
LeavesPooledElements - 2.00 μs - - -
LeavesInPlace - 404.04 ns - - -
NodeNew - 456.01 ns - 468.28 ns -
NodePooled - 159.17 ns - - -
PlainNew - 2.20 μs - 2.76 μs -
DeserializePolymorphicClassArray - 216.65 μs - 295.36 μs ...
Read more

v4.0.0-beta.47

v4.0.0-beta.47 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Feb 00:01

What's Changed

✨ Features

📦 analyzer

📦 ci

📦 core

📦 deserializer

📦 formatter

📦 generator

📦 release

📦 serialization

📦 tests

📦 workflow

🐛 Bug Fixes

🔧 General

📦 cache

📦 ci

📦 generator

📦 release

📦 serialization

📦 tests

📦 workflow

⚡ Performance

🔧 General

📦 benchmark

📦 cache

📦 collections

📦 core

📦 deserializer

📦 generator

📦 serialization

Read more