Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a reference to MessagePack implementation #14

Merged
merged 23 commits into from
Nov 1, 2024

Conversation

AArnott
Copy link
Contributor

@AArnott AArnott commented Oct 24, 2024

This pull request includes updates to the README.md file, enhancements to the SpanDictionary class, and improvements to the equality members of the BaseClass and DerivedClass in the test types.

Documentation Updates:

  • README.md: Added a new section listing known libraries based on TypeShape, including Nerdbank.MessagePack.

Code Enhancements:

Test Improvements:

It's very rough and limited. And it assumes eiriktsarpalis#12 is addressed.
@AArnott
Copy link
Contributor Author

AArnott commented Oct 24, 2024

Incidentally, upgrading the baseline from MessagePack v2 to v3 produces this:

Serialization

Method Mean Error StdDev Ratio Allocated Alloc Ratio
Serialize_TypeShape 130.9 ns 0.37 ns 0.33 ns 0.95 - NA
Serialize_Library 137.6 ns 0.23 ns 0.21 ns 1.00 - NA

This improves serialization slightly, making TypeShape fair slightly worse by comparison.

Deserialization

Method Mean Error StdDev Ratio Allocated Alloc Ratio
Deserialize_TypeShape 148.1 ns 0.63 ns 0.56 ns 1.21 32 B 1.00
Deserialize_Library 122.2 ns 0.74 ns 0.61 ns 1.00 32 B 1.00

Deserialization improves significantly, making TypeShape look quite a bit worse. This is due to using a dictionary to look up property names, which the AOT formatter avoids.

@AArnott
Copy link
Contributor Author

AArnott commented Oct 24, 2024

Serialization is somehow faster than the Ref.Emit specialized code produced by the MessagePack library. I don't know how that's possible.

I suspect it's because in this version, we call MessagePackWriter.Write(string) directly rather than including a level of indirection that allows for some other serializer to hijack that operation at runtime to serialize something else (e.g. so that a string is only serialized once).

private delegate T? FormatDeserialize<T>(ref MessagePackReader reader, MessagePackSerializerOptions options);
private delegate void FormatSerialize<T>(ref MessagePackWriter writer, T? value, MessagePackSerializerOptions options);

private sealed class Formatter<T>(FormatDeserialize<T> deserialize, FormatSerialize<T> serialize) : IMessagePackFormatter<T?>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why you went for a delegate adapter, but it does result in two layers of indirection for every call, which adds up as the object graph is being traversed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.
Can you review all the places where formatters.GetOrAdd appears in my code and tell me if I'm doing it right? I don't understand why this pattern is required, so I'm not sure...

Copy link
Owner

@eiriktsarpalis eiriktsarpalis Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was suggesting that you could make Formatter<T> an abstract class and have individual types and shapes inherit from that. The downside is that you need to factor those into individual classes as opposed to inlining lambda expressions, but on the other hand splitting your code into a folder of Formatters and then a keeping a small visitor that just folds them together should make things more manageable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, I'm not sure what I'd do with more than the two implementations I have.
Maybe I'll need more as I get to supporting non-default constructors, etc. though.

};

internal IMessagePackFormatter<T?> GetFormatter<T>(ITypeShape<T> typeShape)
{
return formatters.GetOrAdd<IMessagePackFormatter<T?>>(typeShape, this, box => new Formatter<T?>(box.Result.Deserialize, box.Result.Serialize));
return formatters.GetOrAdd<IMessagePackFormatter<T?>>(typeShape, this, box => box.Result switch
Copy link
Owner

@eiriktsarpalis eiriktsarpalis Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delayed value factory is a late binding mechanism used when a recursive type is encountered. The idea is that it's creating a wrapper for box.Result which will eventually contain a reference to the final computed value once building has completed.

For this to work, evaluating box.Result needs to be delayed until an actual serialization operation is run. Here's how it's done for CBOR:

https://github.com/eiriktsarpalis/typeshape-csharp/blob/c8c2c397beb472e716b447bb460dea626811ba18/src/TypeShape.Examples/CborSerializer/Converters/DelayedCborConverter.cs#L6-L13

And here's how the object cloner does it:

https://github.com/eiriktsarpalis/typeshape-csharp/blob/ba848263d45d05975fa7cc3a0e601b1bcdca608b/src/TypeShape.Examples/Cloner/Cloner.cs#L62

@AArnott
Copy link
Contributor Author

AArnott commented Oct 24, 2024

@neuecc this should interest you.

@AArnott AArnott changed the title Add a msgpack serializer sample Add a reference to MessagePack implementation Nov 1, 2024
@AArnott AArnott marked this pull request as ready for review November 1, 2024 16:16
@@ -592,15 +592,27 @@ public StructWithDefaultCtor()
}

[GenerateShape]
public partial class BaseClass
public partial class BaseClass : IEquatable<BaseClass>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to factor the IEquatable implementation to a separate set of types so that we coverage for both cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this because I had been testing round-tripping. I'm not sure what case we're not covering when this interface isn't present.
But I did just discover IsEquatable on test cases, which probably was false for this type up to this point and why no one else hit the fact that this class didn't know how to do a by-value compare of itself.
I can remove this change if you'd like. I don't need it any more.

@eiriktsarpalis eiriktsarpalis merged commit 0b03b2a into eiriktsarpalis:main Nov 1, 2024
4 checks passed
@AArnott AArnott deleted the msgpack branch November 1, 2024 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants