-
Notifications
You must be signed in to change notification settings - Fork 55
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 INatsConnectionPool interface #109
Conversation
9b01b4f
to
d12daa9
Compare
@@ -38,12 +38,16 @@ public NatsConnectionPool(int poolSize, NatsOptions options, Action<NatsConnecti | |||
} | |||
} | |||
|
|||
INatsConnection INatsConnectionPool.GetConnection() => GetConnection(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opted for an explicit interface implementation so clients could still call the non-virtual GetConnection
if they are concerned about perf. That being said, I am almost certain that at least the virtual call to GetConnection
could be inlined with GDV in .NET 8.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked into the hosting APIs properly yet. Would this be on a hot path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for the DI integration in NATS.Client.Hosting. It's always resolving NatsConnectionPool
and than calling the non-virtual GetConnection() method.
Everything else depends on usage, i.e. if NatsConnection
or INatsConnection
is injected.
If people have DI resolution on their hotpath they have bigger performance issues than some virtual calls I suppose.
The only benefit here is when directly using NatsConnectionPool
and frequently invoking a method on the returned I/NatsConnection
. If we don't use an explicit interface those would be virtual calls, e.g. something like this:
var conn = connPool.GetConnection();
while(true)
{
// This would either be non-virtual if conn was retrieved from
// NatsConnectionPool and virtual if it was retrieved from INatsConnectionPool
await conn.PublishAsync();
}
But I'm not convinced this is actually worth the ugliness of explicit interface impl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like still using callvirt:
Unless I'm missing the point of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's callvirt in MSIL, but since JIT sees the exact type in M_2, it uses a uses a non-virtual call. I find it easier to see in Compiler Explorer: https://godbolt.org/z/fYEnxEqo9
Here, C.M()
got inlined which only happens because the call is non-virtual.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But for me it's fine either way, only having those methods in the interface or having the explicit interface implementation in addition to the methods on the class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's callvirt in MSIL, but since JIT sees the exact type in M_2, it uses a uses a non-virtual call. I find it easier to see in Compiler Explorer: https://godbolt.org/z/fYEnxEqo9
I see. However commenting out explicit implementation isn't making any difference https://godbolt.org/z/6Y8K9E3vf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But for me it's fine either way, only having those methods in the interface or having the explicit interface implementation in addition to the methods on the class.
We do care a lot about performance and this won't make any functional difference here, so if you think there may still be benefit, please add a few lines of comment explaining why we used an explicit interface here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However commenting out explicit implementation isn't making any difference
The "problem" is, that w/o the explicit implementations, clients can't obtain a NatsConnection
instance but only an INatsConnection
and thus all calls on that connection instance will be virtual.
I removed it anyway, because the longer I look at it, the more it feels like premature optimization. It can still be added anytime.
Some figures:
Method | Job | Runtime | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|---|---|
CallDirect | .NET 6.0 | .NET 6.0 | 1.0188 ns | 0.0365 ns | 0.0342 ns | 1.0136 ns | 1.00 | 0.00 |
CallVirtual | .NET 6.0 | .NET 6.0 | 2.6844 ns | 0.0399 ns | 0.0373 ns | 2.6744 ns | 2.64 | 0.10 |
CallDirect | .NET 7.0 | .NET 7.0 | 0.5650 ns | 0.0365 ns | 0.0324 ns | 0.5700 ns | 1.00 | 0.00 |
CallVirtual | .NET 7.0 | .NET 7.0 | 2.1274 ns | 0.0560 ns | 0.0437 ns | 2.1451 ns | 3.75 | 0.23 |
CallDirect | .NET 8.0 | .NET 8.0 | 0.2981 ns | 0.0077 ns | 0.0068 ns | 0.2977 ns | 1.00 | 0.00 |
CallVirtual | .NET 8.0 | .NET 8.0 | 0.3829 ns | 0.0284 ns | 0.0251 ns | 0.3783 ns | 1.28 | 0.08 |
CallDirect | NativeAOT 7.0 | NativeAOT 7.0 | 1.5757 ns | 0.0573 ns | 0.0926 ns | 1.6269 ns | 1.00 | 0.00 |
CallVirtual | NativeAOT 7.0 | NativeAOT 7.0 | 1.1989 ns | 0.0529 ns | 0.0650 ns | 1.2314 ns | 0.78 | 0.06 |
CallDirect | NativeAOT 8.0 | NativeAOT 8.0 | 0.3246 ns | 0.0153 ns | 0.0143 ns | 0.3159 ns | 1.00 | 0.00 |
CallVirtual | NativeAOT 8.0 | NativeAOT 8.0 | 0.5110 ns | 0.0382 ns | 0.0886 ns | 0.5193 ns | 1.42 | 0.39 |
Benchmark code
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.NativeAot70)]
[SimpleJob(RuntimeMoniker.NativeAot80)]
public class Calls
{
public interface IFoo
{
int Do();
}
public sealed class Foo : IFoo
{
public int Do() => 42;
}
public static IFoo _virtualFoo = new Foo();
public static Foo _directFoo = new Foo();
[Benchmark(Baseline = true)]
public int CallDirect()
{
return Do(_directFoo);
[MethodImpl(MethodImplOptions.NoInlining)]
static int Do(Foo foo)
{
return foo.Do();
}
}
[Benchmark]
public int CallVirtual()
{
return Do(_virtualFoo);
[MethodImpl(MethodImplOptions.NoInlining)]
static int Do(IFoo foo)
{
return foo.Do();
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quite a difference in benchmark results, double, triple in some cases. Definitely we must use it on hot paths. But I agree with you maybe not necessary for this scenario. Thanks for your hard work @jasper-d we really do appreciate your contribution.
btw @jasper-d please sign your commits. |
using NATS.Client.Core.Internal; | ||
|
||
namespace NATS.Client.Core; | ||
|
||
public partial class NatsConnection | ||
{ | ||
internal async ValueTask<NatsSub> RequestSubAsync( | ||
public async ValueTask<NatsMsg<TReply?>?> RequestAsync<TRequest, TReply>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we definitely set on making moving these to NatsConnection
? Or should we try to convert them to extension methods that extend INatsConnection
instead?
The extension methods could be nice for someone else who implements INatsConnection
but I'm not sure if that will be common
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made changes to keep the four extension methods that don't need to be instance members as extension methods and only moved those four to the interface that access internal methods (i.e. RequestSubAsync
).
I think outside of tests it's highly unlikely that anyone want's to implement INatsConnection.
d12daa9
to
37a4b00
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM @caleblloyd can you have a look as well please?
Looks good, I think that the Request-Reply methods should probably either All be in the interface, or All be in extensions of INatsConneciton. But we can make a decision on that and move those around in a follow-up |
#105