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

Elide repeated Action-delegate allocation by caching it #1324

Merged
merged 2 commits into from
Jan 8, 2022

Conversation

gfoidl
Copy link
Member

@gfoidl gfoidl commented Jan 4, 2022

Here C# compiler has too little knowledge about the usage of the delegate, we know better so cache it on our own to avoid the repeated allocation.

From the profiling session in #1323 we see that there are ~100.000 allocations of the Action-delegate. They come from cancellationToken.Register and the C# compiler can't cache the method-group here.

With the change from this PR these allocations are gone. The following images shows the changes of #1323 combined with this one:

perf_02

So for the client publishing use case only the allocations for tasks, the publish packet, and the publish result show up.
To avoid these allocations I fear some bigger refactorings are needed, and thats a different story...

Here C# compiler has too little knowledge about the usage of the delegate, we know better so cache it on our own to avoid the repeated allocation.
@@ -150,7 +156,7 @@ public async Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancellat
}

// Workaround for: https://github.com/dotnet/corefx/issues/24430
Copy link
Member Author

Choose a reason for hiding this comment

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

Question: starting with .NET Core 3.0 this workaround isn't needed anymore, so the code could be conditionally compiled for a "legacy" target and for new targets.

W/o the workaround perf in general should be better, as the cost of creating, and disposing the cancellationtoken-registration can be avoided.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If it is fixed we should add a compiler switch (NETCOREAPP3_0_OR_GREATER).

@@ -150,7 +156,7 @@ public async Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancellat
}

// Workaround for: https://github.com/dotnet/corefx/issues/24430
using (cancellationToken.Register(Dispose))
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the above code I agree that the lambda is not required but here we already have a method without wrapping it. Do we really need that optimization here as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

From my profiling it showed up in WriteAsync (L187), so it's just duplicated here.

But when the conditional compilation is added for .NET Core 3.0 and newer, this won't be hit. So for the legacy code we could

  • keep the state of master-branch
  • keep the cached delegate of PR's current state

I'm leaning to the latter, as the change isn't huge and so older targets can benefit from that change too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, now I see your concern. Yes we need it. Whether it's a delegate created from a method group or a lambda, the delegate won't be cached. Checked the IL for both.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK then we should keep it.

@@ -112,7 +118,7 @@ public async Task ConnectAsync(string host, int port, CancellationToken cancella
_networkStream?.Dispose();

// Workaround for: https://github.com/dotnet/corefx/issues/24430
using (cancellationToken.Register(() => _socket.Dispose()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the bug also fixed in this case so adding the pragma here works as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

Socket.ConnectAsync(string, int, CancellationToken) exists only starting from .NET 6.
So for the moment we have to stay at the current code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK.

@chkr1011
Copy link
Collaborator

chkr1011 commented Jan 8, 2022

Thank you for your contribution. Good work 👍

@chkr1011 chkr1011 merged commit bd40e10 into dotnet:master Jan 8, 2022
chkr1011 added a commit that referenced this pull request Jan 8, 2022
commit 27592f1
Author: Christian <[email protected]>
Date:   Sat Jan 8 15:36:35 2022 +0100

    Update MQTTnet.nuspec

commit c6f4b59
Author: Christian <[email protected]>
Date:   Sat Jan 8 15:35:47 2022 +0100

    Increase delay for Keep Alive checks to decrease CPU load.

commit b362b2a
Author: Christian <[email protected]>
Date:   Sat Jan 8 14:35:25 2022 +0100

    Make logger parameters generic.

commit 7a72a8d
Author: Christian <[email protected]>
Date:   Sat Jan 8 14:07:57 2022 +0100

    Update MQTTnet.nuspec

commit bd40e10
Author: Günther Foidl <[email protected]>
Date:   Sat Jan 8 14:00:39 2022 +0100

    Elide repeated Action-delegate allocation by caching it (#1324)

    * Elide repeated Action-delegate allocation by caching it

    Here C# compiler has too little knowledge about the usage of the delegate, we know better so cache it on our own to avoid the repeated allocation.

    * Use conditional compilation

commit 4b44a2b
Author: Christian <[email protected]>
Date:   Fri Jan 7 18:01:03 2022 +0100

    Remove appveyor.

commit 215f24a
Author: Christian <[email protected]>
Date:   Fri Jan 7 17:52:16 2022 +0100

    Create CODE-OF-CONDUCT.md
@gfoidl gfoidl deleted the elide-action-allocation branch January 9, 2022 15:05
@yyjdelete
Copy link
Contributor

@gfoidl
Maybe it's better to use static Action instead of instance ones, and pass _socket or this as state for CancellationToken.Register()

static readonly Action<object> _socketDisposeAction = state => ((Socket)state).Dispose();

using (cancellationToken.Register(_socketDisposeAction, _socket))

@gfoidl
Copy link
Member Author

gfoidl commented Jan 14, 2022

@yyjdelete that's another way to do it, but I don't know if it is actually "better".
Due the static field the instance gets smaller a few bytes, but these objects are intended to live longer, so it won't matter much.
Calling a static delegate from an instance method has more runtime overhead (due to shuffle thunks that the runtime needs to create), which won't matter here too (could have an effect on (very) hot code-pathes).

So for the use here it could be done either way. In this PR I just the most easy to write and read way.
But I'm open to change it to your suggestion, leaving the decision to up to others (or you open a PR and it can be discussed further over there).

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.

3 participants