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

Reduce FSM<TState, TData> allocations #6145

Merged
merged 7 commits into from
Oct 7, 2022

Conversation

Aaronontheweb
Copy link
Member

@Aaronontheweb Aaronontheweb commented Oct 6, 2022

Changes

Reduced some unnecessary allocations inside the FSM<TState, TData> class.

Checklist

For significant changes, please ensure that the following have been completed (delete if not relevant):

Latest v1.4 Benchmarks

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.2006 (21H2)
AMD Ryzen 7 1700, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.201
  [Host]     : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
  DefaultJob : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT

Method MsgCount Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
BenchmarkFsm 1000000 772.8 ms 13.20 ms 12.35 ms 1.74 0.06 68000.0000 4000.0000 1000.0000 287 MB
BenchmarkUntyped 1000000 444.3 ms 8.88 ms 9.50 ms 1.00 0.00 12000.0000 4000.0000 1000.0000 56 MB

This PR's Benchmarks

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.2006 (21H2)
AMD Ryzen 7 1700, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.201
  [Host]     : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
  DefaultJob : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT

Method MsgCount Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
BenchmarkFsm 1000000 662.5 ms 12.99 ms 13.34 ms 1.47 0.05 35000.0000 4000.0000 1000.0000 150 MB
BenchmarkUntyped 1000000 453.5 ms 8.97 ms 11.35 ms 1.00 0.00 13000.0000 3000.0000 1000.0000 56 MB

48% memory reduction, 14% throughput improvement

@Aaronontheweb Aaronontheweb added akka-actor perf akka.net v1.4 Issues affecting Akka.NET v1.4 labels Oct 6, 2022
@Aaronontheweb Aaronontheweb added this to the 1.4.44 milestone Oct 6, 2022
Copy link
Member Author

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

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

Described API and memory allocation changes.

@@ -736,7 +736,7 @@ namespace Akka.Actor
public override int GetHashCode() { }
public override string ToString() { }
}
public sealed class Event<TD> : Akka.Actor.INoSerializationVerificationNeeded
public struct Event<TD> : Akka.Actor.INoSerializationVerificationNeeded
Copy link
Member Author

Choose a reason for hiding this comment

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

Technically a binary breaking change maybe, but it's source compatible.

@@ -773,10 +773,10 @@ namespace Akka.Actor
{
public static Akka.Actor.FSMBase.StateTimeout Instance { get; }
}
public class State<TS, TD> : System.IEquatable<Akka.Actor.FSMBase.State<TS, TD>>
public sealed class State<TS, TD> : System.IEquatable<Akka.Actor.FSMBase.State<TS, TD>>
Copy link
Member Author

Choose a reason for hiding this comment

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

sealed the State class for better perf.

@@ -435,7 +435,7 @@ public class State<TS, TD> : IEquatable<State<TS, TD>>
/// <param name="notifies">TBD</param>
public State(TS stateName, TD stateData, TimeSpan? timeout = null, Reason stopReason = null, IReadOnlyList<object> replies = null, bool notifies = true)
{
Replies = replies ?? new List<object>();
Replies = replies ?? Array.Empty<object>();
Copy link
Member Author

Choose a reason for hiding this comment

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

Eliminated unnecessary List<object> allocation.

@@ -539,15 +540,16 @@ public State(TS stateName, TD stateData, TimeSpan? timeout = null, Reason stopRe
/// </summary>
internal State<TS, TD> WithNotification(bool notifies)
{
// don't bother allocating even a stack type if the notifies value is identical.
if (Notifies == notifies)
Copy link
Member Author

Choose a reason for hiding this comment

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

Don't return a new copy of this object if the current Notifies value matches the new one (it usually does.)

@@ -585,26 +587,26 @@ public override int GetHashCode()
/// which allows pattern matching to extract both state and data.
/// </summary>
/// <typeparam name="TD">The state data for this event</typeparam>
public sealed class Event<TD> : INoSerializationVerificationNeeded
public readonly struct Event<TD> : INoSerializationVerificationNeeded
Copy link
Member Author

Choose a reason for hiding this comment

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

We allocate one of these for every message we process, so changing to a readonly struct here greatly reduces Gen 0 / 1 GC.

@@ -750,7 +752,7 @@ public void StartWith(TState stateName, TData stateData, TimeSpan? timeout = nul
/// <returns>Descriptor for staying in the current state.</returns>
public State<TState, TData> Stay()
{
return GoTo(_currentState.StateName).WithNotification(false);
return _currentState.WithNotification(false);
Copy link
Member Author

Choose a reason for hiding this comment

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

Biggest memory savings - don't allocate a new State object each time we call Stay. Re-use the same value.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW, there's no benefit to copying the same state over and over again here. The Reason, Replies properties et al don't have any effects when used here in combination with WithNotification(false).

@Aaronontheweb
Copy link
Member Author

Looked at making State<TData, TState> into a struct as well but that's a significant breaking API change (we have lots of FSM behaviors that return null in order to trigger Unhandled behavior) that doesn't yield much benefit once we stop returning new states during Stay() operations.

@Aaronontheweb
Copy link
Member Author

Looks like this change definitely caused an issue with the FSMTimingSpec. Going to need to look at that before I ask for review.

@Aaronontheweb
Copy link
Member Author

Looks like this change definitely caused an issue with the FSMTimingSpec. Going to need to look at that before I ask for review.

Spec could also be racy - worked on my machine.

@Aaronontheweb
Copy link
Member Author

Looks like this change definitely caused an issue with the FSMTimingSpec. Going to need to look at that before I ask for review.

Spec could also be racy - worked on my machine.

Nope, definitely an error related to my changes. Will investigate.

@Aaronontheweb
Copy link
Member Author

Benchmarks based on latest changes.

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.2006 (21H2)
AMD Ryzen 7 1700, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.201
  [Host]     : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
  DefaultJob : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT

Method MsgCount Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
BenchmarkFsm 1000000 641.1 ms 12.73 ms 14.15 ms 1.35 0.05 35000.0000 4000.0000 1000.0000 150 MB
BenchmarkUntyped 1000000 474.1 ms 9.38 ms 10.04 ms 1.00 0.00 12000.0000 4000.0000 1000.0000 56 MB

Copy link
Contributor

@Arkatufus Arkatufus left a comment

Choose a reason for hiding this comment

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

LGTM

@Aaronontheweb Aaronontheweb enabled auto-merge (squash) October 7, 2022 15:28
@Aaronontheweb Aaronontheweb merged commit 234bb8f into akkadotnet:v1.4 Oct 7, 2022
@Aaronontheweb Aaronontheweb deleted the fsm-actor-allocations branch October 7, 2022 15:59
Aaronontheweb added a commit to Aaronontheweb/akka.net that referenced this pull request Oct 8, 2022
* close akkadotnet#2560 - added performance benchmarks for FSM

* Improved FSM memory consumption

* Made `Event` a `readonly struct`
* Eliminated unnecessary `List<object>` allocations
* Cleaned up XML-DOC comments

* don't return new `State<TState, TData>` during `Stay()`

* API approvals

* fixed `State<TS.,TD>` errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
akka.net v1.4 Issues affecting Akka.NET v1.4 akka-actor perf
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants