Skip to content

Commit

Permalink
eliminate ActorPath.ToSerializationFormat UID allocations (#6195)
Browse files Browse the repository at this point in the history
* eliminate `ActorPath.ToSerializationFormat` UID allocations

Used some more `Span<char>` 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
  • Loading branch information
Aaronontheweb committed Nov 29, 2022
1 parent b17ce60 commit 37179fe
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
34 changes: 34 additions & 0 deletions src/benchmark/Akka.Benchmarks/Utils/SpanHackBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// //-----------------------------------------------------------------------
// <copyright file="SpanHackBenchmarks.cs" company="Akka.NET Project">
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2022 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

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<char> buffer = stackalloc char[22];
return SpanHacks.TryFormat(Formatted, 0, ref buffer);
}
}
}
11 changes: 11 additions & 0 deletions src/core/Akka.Tests/Actor/ActorPathSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
52 changes: 42 additions & 10 deletions src/core/Akka/Actor/ActorPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,15 @@ public static bool TryParseParts(ReadOnlySpan<char> path, out ReadOnlySpan<char>
/// </summary>
/// <param name="prefix">the address or empty</param>
/// <returns> System.String. </returns>
private string Join(ReadOnlySpan<char> prefix)
private string Join(ReadOnlySpan<char> prefix, long? uid = null)
{
void AppendUidSpan(ref Span<char> writeable, int startPos, int sizeHint)
{
if (uid == null) return;
writeable[startPos] = '#';
SpanHacks.TryFormat(uid.Value, startPos+1, ref writeable, sizeHint);
}

if (_depth == 0)
{
Span<char> buffer = prefix.Length < 1024 ? stackalloc char[prefix.Length + 1] : new char[prefix.Length + 1];
Expand All @@ -576,17 +583,28 @@ private string Join(ReadOnlySpan<char> 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<char> buffer = totalLength < 1024 ? stackalloc char[totalLength] : new char[totalLength];
prefix.CopyTo(buffer);

var offset = buffer.Length;
ReadOnlySpan<char> 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));
Expand Down Expand Up @@ -676,7 +694,12 @@ public override bool Equals(object obj)
/// <returns> System.String. </returns>
public string ToStringWithAddress()
{
return ToStringWithAddress(_address);
return ToStringWithAddress(_address, false);
}

private string ToStringWithAddress(bool includeUid)
{
return ToStringWithAddress(_address, includeUid);
}

/// <summary>
Expand All @@ -685,7 +708,7 @@ public string ToStringWithAddress()
/// <returns>TBD</returns>
public string ToSerializationFormat()
{
return AppendUidFragment(ToStringWithAddress());
return ToStringWithAddress(true);
}

/// <summary>
Expand All @@ -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;
}

Expand All @@ -718,16 +740,26 @@ private string AppendUidFragment(string withAddress)
/// <param name="address"> The address. </param>
/// <returns> System.String. </returns>
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);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/core/Akka/Akka.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TargetFrameworks>$(NetStandardLibVersion)</TargetFrameworks>
<PackageTags>$(AkkaPackageTags)</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>7.2</LangVersion>
<LangVersion>9</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
98 changes: 95 additions & 3 deletions src/core/Akka/Util/SpanHacks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Akka.Util
{
Expand Down Expand Up @@ -28,7 +26,101 @@ public static int Parse(ReadOnlySpan<char> 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' };

/// <summary>
/// Can replace with int64.TryFormat in later versions of .NET.
/// </summary>
/// <param name="i">The integer we want to format into a string.</param>
/// <param name="startPos">Starting position in the destination span we're going to write from</param>
/// <param name="span">The span we're going to write our characters into.</param>
/// <param name="sizeHint">Optional size hint, in order to avoid recalculating it.</param>
/// <returns></returns>
public static int TryFormat(long i, int startPos, ref Span<char> 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;
}

/// <summary>
/// How many characters do we need to represent this int as a string?
/// </summary>
/// <param name="i">The int.</param>
/// <returns>Character length.</returns>
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;
}
}

/// <summary>
Expand Down

0 comments on commit 37179fe

Please sign in to comment.