Closed parched closed 6 months ago
Thanks for the contribution! Give me a couple of days to mull this over; FlatSharp's unions used to support behavior similar to this. The reason that I moved away from it was for performance reasons. Basically, if you implement your visitor as a struct, the JIT can make things faster by removing virtual method calls and object allocations for the delegates.
The other is that newer syntax innovations have made an interface implementation lighter weight than it used to be. In terms of lines of code, this isn't that different than inline delegates:
class MyClass
{
void DoSomething(SomeUnion union)
{
union.Accept(new SimpleVisitor());
}
private struct SimpleVisitor : SomeUnion.Visitor<int>
{
public int VisitDog(Dog d) => d.Age;
public int VisitCat(Cat c) => c.LivesRemaining;
}
}
The general rule I try to follow is that I assume people are using FlatBuffers because they care about performance, so I try to structure the APIs to encourage performance.
That said, I completely see your reasoning. My day job isn't terribly performance-sensitive, so I'd probably also be irritated that these unions don't support a .Match
pattern :)
Fair enough, I'll create a benchmark to see how much difference there is. As I understand, the object allocations for the delegate shouldn't be a problem as they'll be lifted to static members if they don't capture anything.
Did you ever get results of the benchmark? Another way to do this would be to have a DelegateVisitor
implementation that is easy to construct.
No I haven't, but I'll have some time coming up next week to look at this upstreaming.
I've looked a little bit at this using LinqPad, and the delegate implementation looks substantially worse in terms of number of bytes emitted by the JITer. You can find a gist here: https://gist.github.com/jamescourtney/1929e73d551bc1e4f73592cfde09d145. The JITer is also not creating static methods for the delegates -- new objects are allocated for each method call.
The short version is that the struct implementation of the visitor has the best characteristics and maximum inlining. I feel pretty strongly that the FlatSharp API should steer users to write things efficiently since that is the point of FlatBuffers :) However, I recognize that the delegate approach is useful for oneoffs or other cases where performance may not matter as much.
Perhaps the question is: "how can we structure the API so that it steers people to do the right thing while making it clear that the delegate approach is not as good?"
We could just add a note in the doc comment for Match
that Visit
should be used for better performance.
For what it's worth, we chose FlatSharp because we wanted a language agnostic IDL with generators for multiple programming languages, but with an idiomatic C# implementation (which is our main language). I didn't find any other options. Performance was just a secondary benefit.
All modified and coverable lines are covered by tests :white_check_mark:
Comparison is base (
6260caf
) 97.31% compared to head (5bf8b7a
) 97.22%. Report is 6 commits behind head on main.
Agree -- You'll want to do a pull from my main branch since I've made some changes to exception handling. You should be able to emulate what I do in Accept
for the new Match
method.
Please also add a comment to both Accept
and Match
that indicates the performance characteristics of each. Use codeWriter.AppendSummaryComment
for this.
Regarding testing, please add some tests in FlatSharpEndToEndTests. There should already be some union cases you can add to. Regarding mutation testing, the pipeline may fail for you. This is done with dotnet-stryker and is the most rigorous set of tests that I've written. It can be a little tricky to get working. Feel free to look at the .yml if you want to give it a shot, otherwise I'm happy to write the tests.
I've implemented this and published in FlatSharp 7.6.0
This is one of the changes we have in the fork we're using at our company.
It adds a
Match
method to unions similar to other unions in the C# ecosystem, e.g., OneOf, dunet.It avoids the need to create a visitor class for one time use.
We use it like this:
If you're happy to accept a change like this. I'll tidy it up a bit and add tests where necessary.