Repository navigation
Releases
v4.0.0-rc.9
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.PresencereplacesNinoHeader.NullFlag, and published layouts sayheader=Presence(NinoTypeLayoutAttribute.ToString(), the Go and Rust layout strings);NullFlagis removed.NinoPolymorphic.TryRegister<T>()andTryRegister(Type)replaceRegister<T>()andRegister(Type), which are removed; they return false when the type cannot be registered.NinoDeserializer.Deserialize<T>(ref Reader reader, ref T value)andDeserializeBoxed(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)]replacesallowInheritance: false(andNinoTypeAttribute.InheritedBySubtypesreplacesAllowInheritance); the old name is removed, and a code fix renames a namedallowInheritance: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,utf8in the published layout); info NINO031 and its code fix rewrite it to[NinoUtf8].WEAK_VERSION_TOLERANCEis removed. Where a project still defines it, error NINO032 marks each type that would turn positional, and its code fix addsformat: NinoFormat.VersionTolerant, which keeps the same bytes (fix all applies it to the whole solution).- Published layouts write the type id as
id=0x...(wastypeId=), andNinoTypeLayoutAttribute's constructor parameter isid, matching itsIdproperty; Go and Rust layout strings useid=too. - Custom formatters implement
Deserialize(ref Reader reader, ref T value): the reader comes first, as inNinoDeserializer; the old order is removed, so swap the two parameters in your formatters. - The code fix for
[NinoType(containNonPublicMembers: ...)](the v3 name) renames it toincludeNonPublicMembers. - 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(wasTYPE_ID), named likeNINO_TYPE_NAMEandNINO_LAYOUT; regenerate, and writeCat::NINO_TYPE_IDwhere code namedCat::TYPE_ID(the body traits'TYPE_IDis 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.Untrustedchecks aboolfield 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
Dictionaryof plain struct values withNinoSettings.Untrusted(such asDictionary<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 nowOption<WideString>/Option<String>/Option<Utf8>. Breaking for Rust code that builds or reads such sets (IndexSet<String>becomesIndexSet<Option<String>>).
Every change between v4 releases: Changelog.
What's Changed since v4.0.0-rc.8
106 commits
⚠️ Breaking changes
- feat(generator)!: remove WEAK_VERSION_TOLERANCE, error NINO032 keeps the bytes (283d53d) by @JasonXuDeveloper
- feat(core)!: formatters read reader-first (12e2c13) by @JasonXuDeveloper
- refactor(rust)!: name nino-gen's inherent type id NINO_TYPE_ID (59090fc) by @JasonXuDeveloper
✨ Features
- feat(core): NinoHeader.Presence replaces NullFlag (4d2bcdf) by @JasonXuDeveloper
- feat(core): NinoPolymorphic.TryRegister replaces Register (8feafb9) by @JasonXuDeveloper
- feat(core): reader-first NinoDeserializer overloads (2ad4410) by @JasonXuDeveloper
- feat(core): NinoType's inheritedBySubtypes replaces allowInheritance (c847405) by @JasonXuDeveloper
- feat(generator): one [NinoUtf8], NINO030 and NINO031 (ad1e713) by @JasonXuDeveloper
- feat(core): a layout's id is id everywhere (856cb95) by @JasonXuDeveloper
- feat(analyzer): NINO033 for attributes on interface members that have no effect (17352e9) by @JasonXuDeveloper
- feat(rust): nino::raw_read! and raw_write! for a raw struct's codecs (eb2e53e) by @JasonXuDeveloper
- feat(rust): nino::type_consts! documents generated types' constants once (4c71fe9) by @JasonXuDeveloper
⚡ Performance
- perf(generator): bind no top-level call again while declarations stay (b0f6820) by @JasonXuDeveloper
- perf(core): check an Untrusted bool in a wider lane's pass (0f40fe5) by @JasonXuDeveloper
🐛 Fixes
- fix(benchmarks): no x86-only probe setup in --verify, so Arm64 shards run (d92f31e) by @JasonXuDeveloper
- fix(analyzer): one diagnostic per member mistake; NINO030 where [NinoUtf8] is dead (8b4896a) by @JasonXuDeveloper
- fix(generator): explicit implementations of [NinoType] interface members (054fe39) by @JasonXuDeveloper
- fix(analyzer): NINO033 says whose member an explicit implementation writes (653031e) by @JasonXuDeveloper
- fix(codegen): Rust body traits carry the type id, as the derive's do (bac1d40) by @JasonXuDeveloper
- fix(core): Untrusted value checks read no stride past a run's last value (668d1ed) by @JasonXuDeveloper
- fix(codegen): Rust set elements may be null (9f42d22) by @JasonXuDeveloper
♻️ Refactoring and cleanup (30)
- refactor(core): read a walked ConcurrentDictionary through ReadConcurrent (b5562c9) by @JasonXuDeveloper
- refactor(core): one startup check per table kind for reference and string keys (2330cfd) by @JasonXuDeveloper
- refactor(core): split NinoLayout.Compute into its steps (9ce4524) by @JasonXuDeveloper
- refactor(aspnetcore): split NinoBodyReader by job (6ca7bc2) by @JasonXuDeveloper
- refactor(core): move NinoBlocks into its own file (dd36cb4) by @JasonXuDeveloper
- refactor(core): rename the internal formatter carrier to NinoFormatterBase (567dc7a) by @JasonXuDeveloper
- refactor(generator): shorter names in generated code, one alias rule (234c38a) by @JasonXuDeveloper
- refactor(generator): a formatter's twins call its PrefetchMembers (a728bef) by @JasonXuDeveloper
- refactor(generator): alias NinoBlocks where a file repeats it (f6c657f) by @JasonXuDeveloper
- refactor(generator): end a keyed member once in the fallback loop (6a9dbc5) by @JasonXuDeveloper
- refactor(codegen): number nino-gen's shared functions by shape (5287c32) by @JasonXuDeveloper
- refactor(generator): one check per C# constraint, override walk and ValueTuple test (4e888b8) by @JasonXuDeveloper
- refactor(generator): resolve a type's format once (aac4299) by @JasonXuDeveloper
- refactor(generator): carry one NinoFormat in TypeModel (8372069) by @JasonXuDeveloper
- refactor(generator): name the pipeline's stages (fab572a) by @JasonXuDeveloper
- refactor(core): NinoBlocks.FixedSize / WriteFixed / ReadBlock (7b95cd7) by @JasonXuDeveloper
- refactor(core): one wrapper for the startup checks of runtime internals (2fa7f97) by @JasonXuDeveloper
- refactor(core): NinoSettings sets every limit through one helper (88a5c7e) by @JasonXuDeveloper
- refactor(rust): one write_option for types whose null is the null length (d91aff1) by @JasonXuDeveloper
- refactor(generator): one NinoType stream, one walk over an assembly's types (e8c4b6e) by @JasonXuDeveloper
- refactor(generator): ReadMembers takes the reader first (a330764) by @JasonXuDeveloper
- refactor(generator): shorter, plainer emitted text (3258d92) by @JasonXuDeveloper
- refactor(codegen): no redundant parentheses in the one-item tuple's block map (2be45de) by @JasonXuDeveloper
- refactor(codegen): the targets share how a rendering starts (39aaebd) by @JasonXuDeveloper
- refactor(generator): one CallType for the type a top-level call needs (e5cca99) by @JasonXuDeveloper
- refactor(core): Nino.Core runs the PrefetchMembers loop over a run (a3dbddc) by @JasonXuDeveloper
- refactor(generator): file-scoped NinoGen namespace from C# 10 (3c5b038) by @JasonXuDeveloper
- refactor(generator): registry passes the switched id, not its literal again (8eb5d72) by @JasonXuDeveloper
- refactor(analyzer): title NINO032 by the mistake, not by the history (f94e789) by @JasonXuDeveloper
- refactor(generator): register a concrete [NinoType] with one NinoTypes call (44a18d3) by @JasonXuDeveloper
📖 Documentation (36)
- ...
v4.0.0-rc.8
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 aTimeOnly,Complex,Int128orUInt128after 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 otherT?sequences of a raw[NinoType]struct whose formatter the runtime resolver builds (types no generated code names, serialized byTypeor 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 anInt128orUInt128now 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
stringmember and element (andStringBuilder,TimeZoneInfo's id,[NinoUtf8]strings) is written with a 1, 3 or 5 byte header instead of a 4-byte one: null is00,""is01,"abc"is07 61 62 63, and a string of 64 to 32,767 chars starts with80and 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 aDateTimeat 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
DateTimewhose 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, packagenino) 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.Marshalandnino.Unmarshalread and write tagged Go structs without generated code, checked against the .NET layout a type declares withNinoLayout(). - 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
serdefeature implementing serde for Nino's value types, andFromStrforDecimal,GuidandBigInteger, so structs with Nino codecs can derive serde as well. - Rust runtime:
#[derive(NinoRead, NinoWrite)](featurederive) 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.Deferrablesays whether a type's bodies nested past 128 class bodies are deferred, and the layout's text form carries it asdeferrable=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'sdeferrableattribute is now optional whenlayoutis given. - Go code generation:
nino-gen --lang gowrites Go structs (withninotags andjson/yamltags, camelCase by default) and codecs for an assembly's[NinoType]s, for the Go runtime inruntimes/go. - Rust code generation:
nino-gen --lang rustwrites Rust structs and codecs for an assembly's[NinoType]s, for thenino-serializationcrate, with optional serde derives (--serde). nino-gen(.NET toolNino.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(namespaceNino.Core, derived fromInvalidOperationException) 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) andNinoSettings.MaxCollectionLength(defaultint.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 throwsNinoDataException, so data built to make keys collide (hash flooding) can no longer make reading aDictionary,HashSet, lookup,ImmutableDictionaryorImmutableHashSetquadratic.NinoSettings.Untrusted(off by default): for data from someone who could craft it, the dictionaries and sets Nino creates hash integer,char,Guidandstringkeys with a per-process secret, raw structs are checked as their constructors would (bool,DateTime,decimal,DateOnly,TimeOnly), and bytes left after the value throwNinoDataException. Off, it costs nothing.- Generic
[NinoType]s with non-public members no longer need to bepartialwhen 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/" />(orgo,rust) with nino-gen's options as metadata. Thenino-gencommand is now the Nino.Codegen.Tool dotnet tool. Nino.Codegen replaces Nino.TypeScript, whoseNinoTypeScript*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 asBox<int>as members. - nino-gen's
--maps indexmap|std(MSBuild metadataMaps) for Rust:stdwritesstd::collections::HashMapandHashSetinstead ofindexmap's, with no dependency; they are written inHashMap's iteration order.indexmapstays the default. - nino-gen generates closed generic types in polymorphic and
objectslots (a generic subtype such asBox<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 thederivefeature for#[derive(NinoRead, NinoWrite)]) andnpm install nino-serialization(generate withRuntime="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 asnino::Shared<T>. - nino-gen generates generic types written behind a type id (non-sealed, abstract, interface), the closed types their slots need,
NullableandKeyValuePairtype 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. deserializeExactin the TypeScript runtime (nino-serialization):deserializethat also throws aNinoErrorwhen bytes follow the value, as Go'sDeserializeExactand Rust'sdeserialize_exactdo.Nino.WebGLin the Unity package: WebGL players built with WebAssembly SIMD (Unity 6's Target WebAssembly 2023, or--compiler-flags=-msimd128on 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.NinoKernelsis now a sealed table of native function pointers (delegate* unmanaged[Cdecl]propertiesPermute,Narrow,Widenand the new `...
v4.0.0-rc.7
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
memmovecall, 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>andConcurrentBag<T>of fixed-size elements serialize as one block, andPriorityQueuereads and writes andHashSetwrites 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. Versionand 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>, customICollection<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>orReadOnlySequence<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 aMemory<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>andDictionary<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
ImmutableDictionaryandImmutableHashSetby 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
ImmutableSortedDictionaryandImmutableSortedSetby 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>andSortedDictionary<TKey, TValue>are read by linking their nodes or building their tree directly: 1,000 ints deserialize into aLinkedListin 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 aSortedDictionaryin 17.4 µs instead of 23.9 µs and 16.7 µs instead of 27.1 µs.- Nino.Immutable writes
ImmutableDictionary,ImmutableHashSet,ImmutableSortedDictionaryandImmutableSortedSetstraight 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>andSortedDictionary<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>andDictionary<TKey, TValue>of raw elements are filled directly: 1,000 ints deserialize into aHashSetin 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, whoserep movsbis slower there: 1,000KeyValuePair<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
StringBuilderpast 1 G chars or UTF-8 text past 2 GB throwsOverflowExceptioninstead 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
StringBuilderor multi-dimensional array length is checked against the data before allocating. - Invalid
Version,Uri,CultureInfo,TimeZoneInfoandDateTimeOffsetvalues and null dictionary keys throw Nino'sInvalidOperationException(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
- feat(unity): ship the analyzer's code fixes in the Unity package (d414f2d) by @JasonXuDeveloper
⚡ Performance (65)
- perf(core): serialize collections of fixed-size elements straight into an array of their size (72a0515) by @JasonXuDeveloper
- perf(core): move padded blocks with in-lane shuffles without AVX-512 (3e4ed67) by @JasonXuDeveloper
- perf(core): allocate arrays filled by vector or per-value stores zeroed (47a0baa) by @JasonXuDeveloper...
v4.0.0-rc.6
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
stringencoding 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 thenino.tsruntime, 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 aList<(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
ModuleInitializerAttributepolyfill (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
- feat(aspnetcore): Nino.AspNetCore package for MVC and Minimal APIs (fdcc0e9) by @JasonXuDeveloper
- feat(generator): describe members by their bytes in NinoTypeLayout (f76fa0e) by @JasonXuDeveloper
- feat(core): write Latin-1 strings in one byte a char (9f86e78) by @JasonXuDeveloper
- feat(typescript): generate TypeScript for [NinoType]s after each build (be02a9e) by @JasonXuDeveloper
⚡ Performance
- perf(aspnetcore): read and write bodies without allocating (4a4f136) by @JasonXuDeveloper
- perf(core): write tuples with padding as full vectors of blocks, from an aligned destination (e344f4c) by @JasonXuDeveloper
- perf(typescript): read and write runs of fixed-size values at offsets, and text natively (edbeb0a) by @JasonXuDeveloper
- perf(core): align the destination of vector stores when reading blocks and converting text (790b6ea) by @JasonXuDeveloper
- perf(typescript): slice short text from one decode, and write results in place (1e4fe91) by @JasonXuDeveloper
- perf(core): write groups of blocks with word permutes, in their own method (4123ec4) by @JasonXuDeveloper
- perf(core): allocate arrays of 2 KB or more uninitialized, and inline raw serialization (07c74db) by @JasonXuDeveloper
- perf(core): read groups of blocks with two-source permutes into aligned vectors (570080b) by @JasonXuDeveloper
🐛 Fixes
- fix(typescript): type serialized bytes as Uint8Array<ArrayBuffer> (0e564ad) by @JasonXuDeveloper
- fix(core): narrow and widen strings of 8+ chars without SIMD (fb59816) by @JasonXuDeveloper
- fix(generator): use initializer attributes only where the compilation can access them (c3bab17) by @JasonXuDeveloper
📖 Documentation
- docs(readme): cover everything v4 has released through rc.5 (75bed8a) by @JasonXuDeveloper
- docs: README for the compact strings, Nino.AspNetCore and Nino.TypeScript (c9ee854) by @JasonXuDeveloper
- docs: cover the rc.6 packages and string format in the READMEs and design notes (fd2d02d) by @JasonXuDeveloper
- docs: name ASP.NET Core and TypeScript in the README feature list (221eaaf) by @JasonXuDeveloper
🧪 Tests
- test(generator): expect byte encodings in the cross-assembly layout test (a76c8ae) by @JasonXuDeveloper
- test(benchmarks): TypeScript against JSON in Node and Chromium (1f84bd9) by @JasonXuDeveloper
🔧 Build, CI and dependencies
- ci(benchmarks): attach the Arm64 report when the release notes are full (2dc7e53) by @JasonXuDeveloper
- ci(benchmarks): run the A/B benchmark on x64 runners too (6258f22) by @JasonXuDeveloper
- ci(benchmarks): measure each case in three processes, over five shards per architecture (2b50790) by @JasonXuDeveloper
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 | ... |
v4.0.0-rc.5
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>andBlockingCollection<T>; Nino.Immutable addsImmutableHashSet,ImmutableSortedSet,ImmutableQueue,ImmutableStack,ImmutableDictionaryandImmutableSortedDictionary. - Unity's
AnimationCurve,GradientandRectOffsetserialize out of the box, as members, elements and top-level values,nullandrefreuse included: the Unity package shipsNino.UnityTypes.dllwith 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,000ints, 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), andSortedDictionary<,>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,000intpairs: 65 µs to 15 µs, 4.2x MemoryPack's time). - Deserializing a new
ConcurrentStack<T>is about 1.9x faster (1,000ints: 17.9 µs to 9.3 µs, half of MemoryPack's time). DateTimeOffsetis 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,shortandboolserialize 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, astringand anintserializes 2x faster. SortedSet,SortedDictionary,SortedList,LinkedListandConcurrentDictionaryare read and written through their own fields and methods: with 1,000 elements, aSortedListdeserializes 5.5x faster and aSortedSetserializes 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
- feat(core): formatters for more BCL types and every immutable collection (881a825) by @JasonXuDeveloper
- feat(unity): formatters for AnimationCurve, Gradient and RectOffset (1afb9f6) by @JasonXuDeveloper
📦 Runtime, generator and analyzer
- Collections: block-copy raw elements behind interfaces, Stack and Queue (138d836) by @JasonXuDeveloper
⚡ Performance
- perf(core): DateTimeOffset through its fields, raw tuples as one block (45a120f) by @JasonXuDeveloper
- perf(core): struct elements through their generated formatter, small blocks inline (780c900) by @JasonXuDeveloper
- perf(collections): SortedDictionary, SortedList and ConcurrentDictionary through direct calls (c66b623) by @JasonXuDeveloper
- perf(generator): inline small class bodies wherever the formatter is used (e2a3291) by @JasonXuDeveloper
- perf(core): 64-byte vector copies for writes, AVX-512 masked copy for padded structs (bb60ca1) by @JasonXuDeveloper
- perf(generator): pass structs to their out-of-line body by reference (4b300ba) by @JasonXuDeveloper
- perf(core): allocate strings through FastAllocateString and copy them inline (945dbe2) by @JasonXuDeveloper
- perf(core,generator): copy Reader and Writer a field at a time (516975e) by @JasonXuDeveloper
- perf(collections): give new lists of raw elements an uninitialized backing array (294aa27) by @JasonXuDeveloper
- perf(immutable): read and write immutable collections through arrays (7a6df3e) by @JasonXuDeveloper
- perf(collections): write and read tuples and pairs of raw items as one block (17734a2) by @JasonXuDeveloper
- perf(collections): typed paths for the runtime's sorted, linked and concurrent collections (ad2323f) by @JasonXuDeveloper
- perf(core): SIMD paths for AVX2, Arm64 and WebAssembly (ebb63ac) by @JasonXuDeveloper
- perf(collections): build a new ConcurrentDictionary from the pairs read (35d23c0) by @JasonXuDeveloper
- perf(collections): build a new ConcurrentStack or ConcurrentQueue from an array (39ffade) by @JasonXuDeveloper
- perf(core): faster formatters for the new BCL and immutable types (59aeedc) by @JasonXuDeveloper
🐛 Fixes
- fix(collections): check collection field layouts before using them (205ef0e) by @JasonXuDeveloper
🧪 Tests
- test(core): struct element formatters, sorted and concurrent dictionaries, small blocks (0c93283) by @JasonXuDeveloper
- test(benchmarks): CommonTypesTest, data shaped like that of common applications (04f6a7c) by @JasonXuDeveloper
- test(core): vector copy sizes and inlined class bodies in collections (b9ca93b) by @JasonXuDeveloper
- test(core): lists of wide structs with a property round-trip (38c0f8a) by @JasonXuDeveloper
- test(layout): compare the x86-layout struct as bytes (3cf1b93) by @JasonXuDeveloper
- test(benchmarks): compare the new BCL and immutable types with MemoryPack (4fc4f15) by @JasonXuDeveloper
- test(benchmarks): no MemoryPack BitArray cases, and why for BigInteger (ab1d5de) by @JasonXuDeveloper
🔧 Build, CI and dependencies
- Benchmarks: run only v3's Nino and MemoryPack cases (bc8b839) by @JasonXuDeveloper
- ci(release-notes): add a Performance highlight, use Conventional Commits (15ebc35) by @JasonXuDeveloper
- ci: do not fail the Unity job on a GitHub API or storage timeout (8b51568) by @JasonXuDeveloper
- ci(benchmarks): run the benchmark classes in parallel (341c242) by @JasonXuDeveloper
- ci(benchmarks): an Arm64 benchmark workflow and a collections benchmark (766abc9) by @JasonXuDeveloper
- ci(benchmarks): take several filters in the Arm64 benchmark (827bd60) by @JasonXuDeveloper
- chore(release-notes): highlights for the rc.5 commits that had no trailer (46e7ef1) by @JasonXuDeveloper
- ci(benchmarks): run the release benchmarks on Arm64 too (59745c7) by @JasonXuDeveloper
- ci: no checkout of refs named by workflow inputs (cache poisoning) (e06308f) by @JasonXuDeveloper
- ci(benchmarks): name the compared release, not its Unity alias, in the Arm64 summary (5c292e3) by @JasonXuDeveloper
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) |
|--------------------------------------|----------:|----------:|---------:|------------...
v4.0.0-rc.4
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)andWriter.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
- Core: every Serialize overload takes in T (cab9f16) by @JasonXuDeveloper
- Core: Writer.Write<T>(in T) for unmanaged values (0dde3d7) by @JasonXuDeveloper
📖 Documentation
- Docs: the masked-copy cost is measured on padded SimpleStructs (50da549) by @JasonXuDeveloper
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... |
v4.0.0-rc.3
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,Sizeor[FieldOffset]changes the layout. Raw members now readraw(n){name:type@offset:encoding,...}, and the members of a raw type are its raw encoding (#202). Layout MetadataNinoSerializer.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 toFunc<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
- Generator: publish raw bytes in NinoTypeLayout, read metadata struct layouts (a3eb84e) by @JasonXuDeveloper
- Core: NinoSerializer.Serialize takes in T (bd8c973) by @JasonXuDeveloper
🔧 Build, CI and dependencies
- Release: publish an existing tag again; group library commits (5a7a521) by @JasonXuDeveloper
- Release: generate the highlights from commit trailers (bddf9cc) by @JasonXuDeveloper
- Benchmarks: a v4 time below the floor counts as faster, not on par (4a4c1f3) by @JasonXuDeveloper
- Benchmarks: write the runtime tables of v4-benchmarks.md from the release report (bb66e4f) by @JasonXuDeveloper
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... |
v4.0.0-rc.2
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 positionalrecord 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). nintandnuintmembers 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 MetadataNinoRegistry.Remove(assembly)clears what Nino cached for an assembly, so a collectibleAssemblyLoadContextcan
unload it; on .NET 8 and later this runs when the context unloads (#199).
Unloading Assembliesnew NinoArrayBufferWriter(initialCapacity, pool)rents from yourArrayPool<byte>(#198).
Fixes
- Two
[NinoFormerName]on one type are reported as error NINO026; the generator took the first and
NinoTypeId.OfthrewAmbiguousMatchException(#201).
Every change between v4 releases: Changelog.
What's Changed since v4.0.0-rc.1
18 commits
📦 Runtime, generator and analyzer
- Core: NinoArrayBufferWriter takes a caller-supplied ArrayPool (37f374a) by @JasonXuDeveloper
- Core: release collectible assemblies (NinoRegistry.Remove) (9c2e422) by @JasonXuDeveloper
- Generator: publish each [NinoType]'s wire layout as metadata (bd6a732) by @JasonXuDeveloper
- Generator: read another assembly's auto-properties the same from metadata (a102e38) by @JasonXuDeveloper
- Analyzer: a type takes one [NinoFormerName] (NINO026) (aa50dfc) by @JasonXuDeveloper
🐛 Fixes
- Generator: resolve nint and nuint where they are not IntPtr (011d5ca) by @JasonXuDeveloper
♻️ Refactoring and cleanup
- Remove leftovers (06361bb) by @JasonXuDeveloper
📖 Documentation
- Docs: 3.3.13 is the last release of the v3 API (45abe17) by @JasonXuDeveloper
- Docs: rc.2 release notes; the design covers unloading and layout metadata (e6ff3a9) by @JasonXuDeveloper
🧪 Tests
- Tests: run the automatic-unload test on the .NET 8+ Nino.Core only (80a07ce) by @JasonXuDeveloper
🔧 Build, CI and dependencies
- Build without warnings (b3f5ccf) by @JasonXuDeveloper
- .gitignore: keep ignoring Nino.Core's .AssemblyAttributes (343d388) by @JasonXuDeveloper
- Release: list every commit and contributor in the release notes (46ee0f6) by @JasonXuDeveloper
- Add .gitattributes; tidy the .gitignore files (04b25b6) by @JasonXuDeveloper
- Benchmarks: run v3 once, directly before v4 (1383de3) by @JasonXuDeveloper
- CI: run on ubuntu-26.04 (a789202) by @JasonXuDeveloper
- CI: pin the game-ci CLI version of the Unity tests (195734f) by @JasonXuDeveloper
Other changes
- Unity: keep only the packages the tests use (a2225ad) by @JasonXuDeveloper
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 |
| ... |
v4.0.0-rc.1
Install: dotnet add package Nino --version 4.0.0-rc.1 (Unity package: 4.0.0-preview.201)
What's Changed
- 修改CSharpParser的AddMembers 修复多类或继承类新增对象不兼容反系列化问题 by @HuangUan in #174
- Bump the nuget group with 2 updates by @dependabot[bot] in #175
- Nino v4: rewrite of the runtime and the source generator by @JasonXuDeveloper in #176
- Bump actions/setup-python from 5 to 7 by @dependabot[bot] in #181
- Bump actions/upload-artifact from 4 to 7 by @dependabot[bot] in #177
- Bump actions/setup-dotnet from 4 to 6 by @dependabot[bot] in #178
- Bump actions/checkout from 4 to 7 by @dependabot[bot] in #179
- Bump actions/cache from 4 to 6 by @dependabot[bot] in #180
- Bump BenchmarkDotNet from 0.15.2 to 0.15.8 by @dependabot[bot] in #182
- Bump coverlet.collector from 6.0.4 to 10.1.0 by @dependabot[bot] in #183
- Update NuGet dependencies; README fixes by @JasonXuDeveloper in #190
- Tests: use a patched System.Security.Cryptography.Pkcs by @JasonXuDeveloper in #196
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 | ... |
v4.0.0-beta.47
What's Changed
✨ Features
📦 analyzer
- add validation and tests for NinoRefDeserializationAttribute [3db1a4d] by @claude[bot]
📦 ci
- support comma-separated scopes in release notes [94ef222] by @JasonXuDeveloper
📦 core
- add delegate null checks and conditional Unity code generation [05a4bca] by @JasonXuDeveloper
📦 deserializer
- add ReadOnlySpan overloads for DeserializeRef methods [9d2b0e2] by @claude[bot]
📦 formatter
- implement MemoryPack-inspired high-performance custom formatter system [5d6be51] by @JasonXuDeveloper
📦 generator
- add CodeFixProvider for IDE quick fixes with comprehensive test coverage [d76c2c6] by @JasonXuDeveloper
- add ConcurrentQueue and ConcurrentStack support [a9cddb4] by @JasonXuDeveloper
- add NinoRefDeserializationAttribute for custom object pooling [6d0c185] by @claude[bot]
- add delegate null checks and conditional Unity code generation [05a4bca] by @JasonXuDeveloper
- add incremental source generator for NinoType discovery [c728df5] by @JasonXuDeveloper
- add multi-dimensional array support [47279ab] by @JasonXuDeveloper
- add new collection type generators and optimize unmanaged types [60d4831] by @JasonXuDeveloper
- add source generator diagnostic testing and NINO011 improvements [72ca9c2] by @JasonXuDeveloper
- allow inheritance over
[NinoType][7cf3ae5] by @JasonXuDeveloper - expose NinoBuiltInTypesRegistration as public [b1288f2] by @JasonXuDeveloper
- support NinoTypes from referenced assemblies [a2591b9] by @JasonXuDeveloper
📦 release
- add dual-tag system for OpenUPM compatibility [70914a8] by @JasonXuDeveloper
📦 serialization
- add boxed object support for generic type serialization with restrictions [8fc6476] by @JasonXuDeveloper
📦 tests
- add CodeFixProvider for IDE quick fixes with comprehensive test coverage [d76c2c6] by @JasonXuDeveloper
- add source generator diagnostic testing and NINO011 improvements [72ca9c2] by @JasonXuDeveloper
📦 workflow
- add benchmark posting to secure workflow [4fc1433] by @JasonXuDeveloper
🐛 Bug Fixes
🔧 General
- always commit Unity DLL updates to main branch for all releases [dba0eda] by @JasonXuDeveloper
- clean up temporary branch after Unity DLL push [c4aa14c] by @JasonXuDeveloper
- correct release workflow DLL copy path for Nino.Generator [b58708b] by @JasonXuDeveloper
- ensure Unity DLL updates reach main branch for stable releases [fb0edd9] by @JasonXuDeveloper
- ensure temporary branch cleanup even on push failure [454a334] by @JasonXuDeveloper
- remove extra argument from RegisterSerializer call in NinoBuiltInTypeGenerator [177e35e] by @claude[bot]
- restore complete CLAUDE.md with all original instructions [5aa8f43] by @JasonXuDeveloper
📦 cache
- correct CacheEntry struct accessibility to internal [829adf4] by @claude[bot]
- correct array initialization syntax for compatibility [af7440d] by @claude[bot]
📦 ci
- correct DLL copy paths in release workflow [cb7051d] by @JasonXuDeveloper
- move release tag to Unity DLL commit for UPM compatibility [df7b32c] by @JasonXuDeveloper
- prevent canceling previous CI runs on non-code changes [b8e0cdf] by @JasonXuDeveloper
- remove invalid secrets check in workflow conditional [4d3072e] by @JasonXuDeveloper
- remove invalid secrets check in workflow conditional [5c77066] by @JasonXuDeveloper
📦 generator
- add null checks for Exception.StackTrace in error handling [2cdf362] by @claude[bot]
- add parameter to control RefDeserializationMethod null check [f794a11] by @claude[bot]
- avoid Unsafe.As entry array access on pre-NET5 to fix HybridCLR ArrayTypeMismatchException [f815a1c] by @JasonXuDeveloper
- capture member types from parsed Nino types to include generic field types [6cc5636] by @JasonXuDeveloper
- check type argument accessibility recursively to prevent CS0051 errors [271b99e] by @JasonXuDeveloper
- check type argument accessibility recursively to prevent CS0051 errors [fbaf9d1] by @JasonXuDeveloper
- directly deserialize members instead of calling DeserializeImplRef [3809b14] by @claude[bot]
- ensure all NinoType members use CachedDeserializer [ecca399] by @JasonXuDeveloper
- fix cross-assembly type discovery in incremental builds [e8f089c] by @JasonXuDeveloper
- gracefully handle unbound generic types [55019e3] by @JasonXuDeveloper
- guard newer refs for netstandard2.1 codegen [072f770] by @JasonXuDeveloper
- handle CustomDeserializer in RefDeserializationMethod path [cc68ee3] by @claude[bot]
- implement offset-based UPM preview version increments [6aa92f0] by @JasonXuDeveloper
- improve ArraySegment fast path implementation [37e4d59] by @JasonXuDeveloper
- modify emit namespace and improve GetKind correctness [c7c0a29] by @JasonXuDeveloper
- register optimal methods for sealed and struct types [632d8cd] by @JasonXuDeveloper
- replace C# 10 inferred delegates with direct method calls for C# 9 compatibility [6fb1e84] by @JasonXuDeveloper
- resolve CS8628 error for nullable reference types in collections [e9659fa] by @JasonXuDeveloper
- resolve IDE duplicate code issue with comprehensive deduplication [3fa682c] by @JasonXuDeveloper
- resolve jagged array syntax error in QueueGenerator for NET5_0_OR_GREATER [3b2a575] by @JasonXuDeveloper
- resolve jagged array syntax errors and centralize array creation logic [dfd5150] by @JasonXuDeveloper
- resolve memory leaks and reduce memory footprint in source generators [7cfe212] by @JasonXuDeveloper
- restore member type validation checks [8a590ba] by @JasonXuDeveloper
- suppress CS8669 nullable annotation warnings in generated code [335b43e] by @JasonXuDeveloper
- use CustomDeserializer instead of CustomSerializer for deserializer prefix [52f85cd] by @JasonXuDeveloper
- use DeserializeImplRef instead of unqualified DeserializeRef call [5db595c] by @claude[bot]
- use tempDict for ReadOnlyDictionary with unmanaged KVP [6d20a9f] by @JasonXuDeveloper
📦 release
- pull latest changes before creating tag to prevent CI push failures [397a87c] by @JasonXuDeveloper
📦 serialization
- fix nullable type handling and restore tuple grouping in deserializer [cc675c6] by @JasonXuDeveloper
📦 tests
- correct DeserializeRef API usage and add NinoIgnore to test fields [da6b1a4] by @claude[bot]
📦 workflow
- use GitHub API to download artifacts from workflow_run [ab102aa] by @JasonXuDeveloper
⚡ Performance
🔧 General
- implement three hot-path optimizations (estimated 2-4% improvement) [2ba6b16] by @claude[bot]
- optimize deserializer to use UnsafeRead for unmanaged types [8505fb6] by @JasonXuDeveloper
- simplify call path [74d9767] by @JasonXuDeveloper
📦 benchmark
- enhance polymorphic serialization benchmarks [5d28c4e] by @JasonXuDeveloper
📦 cache
- optimize inline cache to use single struct array (8→4 entries) [3932d63] by @claude[bot]
- relocate inline cache from Writer/Reader to CachedSerializer/CachedDeserializer [213b727] by @claude[bot]
- replace pattern matching with Unsafe.As for delegate casts [390b77a] by @claude[bot]
📦 collections
- reuse backing storage and extend tests [ec6c57c] by @JasonXuDeveloper
📦 core
- add thread-local buffer pooling and volatile cache access [c6bc61f] by @JasonXuDeveloper
- add thread-local cache for polymorphic serialization [519ad0b] by @JasonXuDeveloper
- four micro-optimizations for hot path performance [d5ec96e] by @claude[bot]
- implement lock-free type registration with CAS [c7188b8] by @JasonXuDeveloper
- optimize FastMap by removing System.Numerics dependency [aa99377] by @JasonXuDeveloper
- optimize FastMap hash operations and remove unused Writer methods [eb4a801] by @claude[bot]
- optimize buffer management and array serialization [cbe7303] by @JasonXuDeveloper
- optimize polymorphic serialization with per-instance caching [ca9fdb9] by @JasonXuDeveloper
- remove inline cache from polymorphic serialization [722ca4b] by @JasonXuDeveloper
- use shared volatile cache for polymorphic serialization [3ccbd64] by @JasonXuDeveloper
📦 deserializer
- optimize CachedDeserializer with direct typeId lookup [27159dc] by @JasonXuDeveloper
📦 generator
- add aggressive inlining to SerializeImpl/DeserializeImpl methods [6bbaeea] by @JasonXuDeveloper
- inline sealed type serialization to eliminate delegate overhead [cb7f33c] by @JasonXuDeveloper
- move AggressiveInlining from NinoType impls to built-in types [f5e0d6c] by @JasonXuDeveloper
- optimize HashSet serialization with direct entry access [eaf6222] by @JasonXuDeveloper
- optimize collection deserialization with three-phase ref overload and Array.Resize pattern [c92f070] by @JasonXuDeveloper
- optimize incremental generator with multiple performance improvements [97a211d] by @JasonXuDeveloper
- optimize incremental pipeline and eliminate memory leaks [e632cda] by @JasonXuDeveloper
- optimize polymorphic deserialization with compile-time dispatch [a0c3f1c] by @JasonXuDeveloper
- optimize polymorphic serialization with compile-time code generation [ee49724] by @JasonXuDeveloper
- skip code generation when Nino.Core is not used [21006f3] by @JasonXuDeveloper
📦 serialization
- optimize array ref overloads with Array.Resize and object reuse [ea6c808] by @JasonXuDeveloper
- optimize dictionary...