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

Use Delegate.EnumerateInvocationList instead of Delegate.GetInvocationList #9246

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ThomasGoulet73
Copy link
Contributor

@ThomasGoulet73 ThomasGoulet73 commented Jun 14, 2024

Description

Use Delegate.EnumerateInvocationList instead of Delegate.GetInvocationList. Delegate.GetInvocationList allocates an array on every call while Delegate.EnumerateInvocationList doesn't allocate which makes the code faster and use less memory.

I also used Delegate.HasSingleTarget for places where we only wanted to check if there was only one entry in Delegate.GetInvocationList, which makes it even faster.

Delegate.EnumerateInvocationList and Delegate.HasSingleTarget were added in .Net 9.0 Preview 4 with dotnet/runtime#97683.

Benchmark results for Delegate.EnumerateInvocationList:

Method count eventHandler Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
GetInvocationList 1 Syste(...)Args] [39] 6.159 ns 0.1593 ns 0.2872 ns 1.00 0.00 0.0019 32 B 1.00
EnumerateInvocationList 1 Syste(...)Args] [39] 3.389 ns 0.0120 ns 0.0100 ns 0.55 0.02 - - 0.00
GetInvocationList 5 Syste(...)Args] [39] 24.718 ns 0.2882 ns 0.2555 ns 1.00 0.00 0.0038 64 B 1.00
EnumerateInvocationList 5 Syste(...)Args] [39] 8.205 ns 0.0431 ns 0.0382 ns 0.33 0.00 - - 0.00
GetInvocationList 10 Syste(...)Args] [39] 46.508 ns 0.9130 ns 1.1547 ns 1.00 0.00 0.0062 104 B 1.00
EnumerateInvocationList 10 Syste(...)Args] [39] 14.742 ns 0.0720 ns 0.0602 ns 0.32 0.01 - - 0.00

I used this benchmark:

Code
[MemoryDiagnoser]
public class GetInvocationListVsEnumerateInvocationListBenchmark
{
  public IEnumerable<object[]> GetArguments()
  {
      EventHandler<EventArgs> handler = (s, e) => { };

      yield return [1, handler];

      EventHandler<EventArgs> handler2 = (s, e) => { };

      for (int x = 0; x < 4; x++)
          handler2 += (s, e) => { };

      yield return [5, handler2];

      EventHandler<EventArgs> handler3 = (s, e) => { };

      for (int x = 0; x < 9; x++)
          handler3 += (s, e) => { };

      yield return [10, handler3];
  }

  [Benchmark(Baseline = true)]
  [ArgumentsSource(nameof(GetArguments))]
  public Delegate GetInvocationList(int count, EventHandler<EventArgs> eventHandler)
  {
      Delegate result = null;
      foreach (Delegate value in eventHandler.GetInvocationList())
      {
          result = value;
      }
      return result;
  }

  [Benchmark]
  [ArgumentsSource(nameof(GetArguments))]
  public Delegate EnumerateInvocationList(int count, EventHandler<EventArgs> eventHandler)
  {
      EventHandler<EventArgs> result = null;
      foreach (EventHandler<EventArgs> value in Delegate.EnumerateInvocationList(eventHandler))
      {
          result = value;
      }
      return result;
  }
}

Benchmark results for Delegate.HasSingleTarget:

Method count eventHandler Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
GetInvocationList 1 Syste(...)Args] [39] 6.4361 ns 0.1005 ns 0.0940 ns 1.00 0.0019 32 B 1.00
HasSingleTarget 1 Syste(...)Args] [39] 0.9718 ns 0.0009 ns 0.0008 ns 0.15 - - 0.00
GetInvocationList 5 Syste(...)Args] [39] 19.6740 ns 0.3254 ns 0.2717 ns 1.00 0.0038 64 B 1.00
HasSingleTarget 5 Syste(...)Args] [39] 0.9788 ns 0.0032 ns 0.0030 ns 0.05 - - 0.00
GetInvocationList 10 Syste(...)Args] [39] 38.0967 ns 0.7512 ns 0.7714 ns 1.00 0.0062 104 B 1.00
HasSingleTarget 10 Syste(...)Args] [39] 0.9816 ns 0.0017 ns 0.0014 ns 0.03 - - 0.00

I used this benchmark:

Code
[MemoryDiagnoser]
public class GetInvocationListVsHasSingleTargetBenchmark
{
  public IEnumerable<object[]> GetArguments()
  {
      EventHandler<EventArgs> handler = (s, e) => { };

      yield return [1, handler];

      EventHandler<EventArgs> handler2 = (s, e) => { };

      for (int x = 0; x < 4; x++)
          handler2 += (s, e) => { };

      yield return [5, handler2];

      EventHandler<EventArgs> handler3 = (s, e) => { };

      for (int x = 0; x < 9; x++)
          handler3 += (s, e) => { };

      yield return [10, handler3];
  }

  [Benchmark(Baseline = true)]
  [ArgumentsSource(nameof(GetArguments))]
  public bool GetInvocationList(int count, EventHandler<EventArgs> eventHandler)
  {
      return eventHandler.GetInvocationList().Length != 1;
  }

  [Benchmark]
  [ArgumentsSource(nameof(GetArguments))]
  public bool HasSingleTarget(int count, EventHandler<EventArgs> eventHandler)
  {
      return !eventHandler.HasSingleTarget;
  }
}

Customer Impact

Improved performance and reduced allocations.

Regression

No.

Testing

Local testing

Risk

Low.

Microsoft Reviewers: Open in CodeFlow

@ThomasGoulet73 ThomasGoulet73 requested a review from a team as a code owner June 14, 2024 01:30
@dotnet-policy-service dotnet-policy-service bot added PR metadata: Label to tag PRs, to facilitate with triage Community Contribution A label for all community Contributions labels Jun 14, 2024
Copy link
Contributor

@lindexi lindexi left a comment

Choose a reason for hiding this comment

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

LGMT

@@ -170,12 +170,12 @@ public void RemoveThreadPreprocessMessageHandlerFirst(ThreadMessageEventHandler
{
ThreadMessageEventHandler newHandler = null;

foreach (ThreadMessageEventHandler testHandler in _threadPreprocessMessage.GetInvocationList())
foreach (ThreadMessageEventHandler testHandler in Delegate.EnumerateInvocationList(_threadPreprocessMessage))

Choose a reason for hiding this comment

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

I'd remove the outer null checks as EnumerateInvocationList accepts null and returns an empty enumerator (false). Same in PropertyMetadata. Otherwise LGTM.

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'd rather keep it that way since it would make cases when _threadPreprocessMessage is null slower while maybe making a tiny perf gain by skipping a null check. Your suggestion would require knowing how often it is called with a null delegate vs a non null delegate, which is more complex.

Copy link

Choose a reason for hiding this comment

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

Yes, that is a fine argument. Maybe I'll feel bored someday but probably not.

@miloush
Copy link
Contributor

miloush commented Jun 15, 2024

Just out of curiosity, does subscribing/unsubscribing in the handler not make the enumerator unhappy?

@ThomasGoulet73
Copy link
Contributor Author

Just out of curiosity, does subscribing/unsubscribing in the handler not make the enumerator unhappy?

Delegates are immutable (subscribing/unsubscribing creates a new delegate instance) so I don't believe we can make the enumerator unhappy: https://learn.microsoft.com/en-us/dotnet/api/system.delegate?view=net-8.0#:~:text=Delegates%20are%20immutable%3B%20once%20created%2C%20the%20invocation%20list%20of%20a%20delegate%20does%20not%20change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community Contribution A label for all community Contributions PR metadata: Label to tag PRs, to facilitate with triage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants