From 37179fe751c6f5ceb8411b40b7da010af8eb73d0 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 29 Nov 2022 14:48:11 -0600 Subject: [PATCH] eliminate `ActorPath.ToSerializationFormat` UID allocations (#6195) * eliminate `ActorPath.ToSerializationFormat` UID allocations Used some more `Span` magic to avoid additional allocations when string-ifying `ActorPath` components. * adding `SpanHacks` benchmarks * sped up `Int64SizeInCharacters` * added `TryFormat` benchmarks * fixed n+1 error in jump table * cleaned up `TryFormat` inside `SpanHacks` * fixed `SpanHacks` index calculation * removed BDN results * Update SpanHacks.cs --- .../Utils/FastLazyBenchmarks.cs | 2 +- .../Utils/SpanHackBenchmarks.cs | 34 +++++++ src/core/Akka.Tests/Actor/ActorPathSpec.cs | 11 +++ src/core/Akka/Actor/ActorPath.cs | 52 ++++++++-- src/core/Akka/Akka.csproj | 2 +- src/core/Akka/Util/SpanHacks.cs | 98 ++++++++++++++++++- 6 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 src/benchmark/Akka.Benchmarks/Utils/SpanHackBenchmarks.cs diff --git a/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs index 59524cc250f..f90fc34b91b 100644 --- a/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs +++ b/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs @@ -50,7 +50,7 @@ public int FastLazy_get_value() } [Benchmark] - public int FastLazy_satefull_get_value() + public int FastLazy_stateful_get_value() { return fastLazyWithInit.Value; } diff --git a/src/benchmark/Akka.Benchmarks/Utils/SpanHackBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Utils/SpanHackBenchmarks.cs new file mode 100644 index 00000000000..fcd83ce3391 --- /dev/null +++ b/src/benchmark/Akka.Benchmarks/Utils/SpanHackBenchmarks.cs @@ -0,0 +1,34 @@ +// //----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2022 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Benchmarks.Configurations; +using Akka.Util; +using BenchmarkDotNet.Attributes; + +namespace Akka.Benchmarks.Utils +{ + [Config(typeof(MicroBenchmarkConfig))] + public class SpanHackBenchmarks + { + [Params(0, 1, -1, 1000, int.MaxValue, long.MaxValue)] + public long Formatted { get; set; } + + [Benchmark] + public int Int64CharCountBenchmark() + { + return SpanHacks.Int64SizeInCharacters(Formatted); + } + + [Benchmark] + public int TryFormatBenchmark() + { + Span buffer = stackalloc char[22]; + return SpanHacks.TryFormat(Formatted, 0, ref buffer); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests/Actor/ActorPathSpec.cs b/src/core/Akka.Tests/Actor/ActorPathSpec.cs index b22f2eedc12..27207259380 100644 --- a/src/core/Akka.Tests/Actor/ActorPathSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorPathSpec.cs @@ -22,6 +22,17 @@ public void SupportsParsingItsStringRep() var path = new RootActorPath(new Address("akka.tcp", "mysys")) / "user"; ActorPathParse(path.ToString()).ShouldBe(path); } + + [Theory] + [InlineData(1)] + [InlineData(100)] + [InlineData(int.MaxValue)] + public void SupportsParsingItsStringRepWithUid(int uid) + { + var path = new RootActorPath(new Address("akka.tcp", "mysys", "localhost", 9110)) / "user"; + var pathWithUid = path.WithUid(uid); + ActorPathParse(pathWithUid.ToSerializationFormat()).ShouldBe(pathWithUid); + } private ActorPath ActorPathParse(string path) { diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index fcd185acb6f..09a4c784e1e 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -557,8 +557,15 @@ public static bool TryParseParts(ReadOnlySpan path, out ReadOnlySpan /// /// the address or empty /// System.String. - private string Join(ReadOnlySpan prefix) + private string Join(ReadOnlySpan prefix, long? uid = null) { + void AppendUidSpan(ref Span writeable, int startPos, int sizeHint) + { + if (uid == null) return; + writeable[startPos] = '#'; + SpanHacks.TryFormat(uid.Value, startPos+1, ref writeable, sizeHint); + } + if (_depth == 0) { Span buffer = prefix.Length < 1024 ? stackalloc char[prefix.Length + 1] : new char[prefix.Length + 1]; @@ -576,17 +583,28 @@ private string Join(ReadOnlySpan prefix) totalLength += p._name.Length + 1; p = p._parent; } + + // UID calculation + var uidSizeHint = 0; + if (uid != null) + { + // 1 extra character for the '#' + uidSizeHint = SpanHacks.Int64SizeInCharacters(uid.Value) + 1; + totalLength += uidSizeHint; + } // Concatenate segments (in reverse order) into buffer with '/' prefixes Span buffer = totalLength < 1024 ? stackalloc char[totalLength] : new char[totalLength]; prefix.CopyTo(buffer); - var offset = buffer.Length; - ReadOnlySpan name; + var offset = buffer.Length - uidSizeHint; + // append UID span first + AppendUidSpan(ref buffer, offset, uidSizeHint-1); // -1 for the '#' + p = this; while (p._depth > 0) { - name = p._name.AsSpan(); + var name = p._name.AsSpan(); offset -= name.Length + 1; buffer[offset] = '/'; name.CopyTo(buffer.Slice(offset + 1, name.Length)); @@ -676,7 +694,12 @@ public override bool Equals(object obj) /// System.String. public string ToStringWithAddress() { - return ToStringWithAddress(_address); + return ToStringWithAddress(_address, false); + } + + private string ToStringWithAddress(bool includeUid) + { + return ToStringWithAddress(_address, includeUid); } /// @@ -685,7 +708,7 @@ public string ToStringWithAddress() /// TBD public string ToSerializationFormat() { - return AppendUidFragment(ToStringWithAddress()); + return ToStringWithAddress(true); } /// @@ -700,8 +723,7 @@ public string ToSerializationFormatWithAddress(Address address) // we never change address for IgnoreActorRef return ToString(); } - var withAddress = ToStringWithAddress(address); - var result = AppendUidFragment(withAddress); + var result = ToStringWithAddress(address, true); return result; } @@ -718,16 +740,26 @@ private string AppendUidFragment(string withAddress) /// The address. /// System.String. public string ToStringWithAddress(Address address) + { + return ToStringWithAddress(address, false); + } + + private string ToStringWithAddress(Address address, bool includeUid) { if (IgnoreActorRef.IsIgnoreRefPath(this)) { // we never change address for IgnoreActorRef return ToString(); } + + long? uid = null; + if (includeUid && _uid != ActorCell.UndefinedUid) + uid = _uid; + if (_address.Host != null && _address.Port.HasValue) - return Join(_address.ToString().AsSpan()); + return Join(_address.ToString().AsSpan(), uid); - return Join(address.ToString().AsSpan()); + return Join(address.ToString().AsSpan(), uid); } /// diff --git a/src/core/Akka/Akka.csproj b/src/core/Akka/Akka.csproj index 4b18ab6cdd1..eaeade66477 100644 --- a/src/core/Akka/Akka.csproj +++ b/src/core/Akka/Akka.csproj @@ -7,7 +7,7 @@ $(NetStandardLibVersion) $(AkkaPackageTags) true - 7.2 + 9 diff --git a/src/core/Akka/Util/SpanHacks.cs b/src/core/Akka/Util/SpanHacks.cs index 08464610126..41e7f4e8bf1 100644 --- a/src/core/Akka/Util/SpanHacks.cs +++ b/src/core/Akka/Util/SpanHacks.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Akka.Util { @@ -28,7 +26,101 @@ public static int Parse(ReadOnlySpan str) { if (TryParse(str, out var i)) return i; - throw new FormatException($"[{str.ToString()}] is now a valid numeric format"); + throw new FormatException($"[{str.ToString()}] is not a valid numeric format"); + } + + private const char Negative = '-'; + private static readonly char[] Numbers = { '0','1','2','3','4','5','6','7','8','9' }; + + /// + /// Can replace with int64.TryFormat in later versions of .NET. + /// + /// The integer we want to format into a string. + /// Starting position in the destination span we're going to write from + /// The span we're going to write our characters into. + /// Optional size hint, in order to avoid recalculating it. + /// + public static int TryFormat(long i, int startPos, ref Span span, int sizeHint = 0) + { + var index = 0; + if (i is < 10 and >= 0) + { + span[startPos] = (char)(i+'0'); + return 1; + } + + var negative = 0; + if (i < 0) + { + negative = 1; + i = Math.Abs(i); + } + + var targetLength = sizeHint > 0 ? sizeHint : PositiveInt64SizeInCharacters(i, negative); + + while (i > 0) + { + i = Math.DivRem(i, 10, out var rem); + span[startPos + targetLength - index++ - 1] = (char)(rem+'0'); + } + + if(negative == 1){ + span[0] = Negative; + index++; + } + + return index; + } + + /// + /// How many characters do we need to represent this int as a string? + /// + /// The int. + /// Character length. + public static int Int64SizeInCharacters(long i) + { + // account for negative characters + var padding = 0; + if (i < 0) + { + i *= -1; + padding = 1; + } + + return PositiveInt64SizeInCharacters(i, padding); + } + + public static int PositiveInt64SizeInCharacters(long i, int padding) + { + switch (i) + { + case 0: + return 1; + case < 10: + return 1 + padding; + case < 100: + return 2 + padding; + case < 1000: + return 3 + padding; + case < 10000: + return 4 + padding; + case < 100000: + return 5 + padding; + case < 1_000_000: + return 6 + padding; + case < 10_000_000: + return 7 + padding; + case < 100_000_000: + return 8 + padding; + case < 1_000_000_000: + return 9 + padding; + case < 10_000_000_000: + return 10 + padding; + case < 100_000_000_000: + return 11 + padding; + default: + return (int)Math.Log10(i) + 1 + padding; + } } ///