Skip to content

Commit

Permalink
Performance optimize VectorClock (#4952)
Browse files Browse the repository at this point in the history
* Performance optimize `VectorClock`

* don't cache MD5, but dispose of it

* guarantee disposal of iterators during VectorClock.Compare

* switch to local function for `VectorClock.CompareNext`

* fixed a comparison bug in how versions where compared

* minor cleanup

* replace `KeyValuePair<TKey,TValue>` with `ValueTuple<TKey,TValue>`

Reduced allocations by 90%, decreased execution time from 100ms to ~40ms
  • Loading branch information
Aaronontheweb authored Apr 19, 2021
1 parent 45bbd3b commit 1f81842
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Akka.Benchmarks.Cluster
[Config(typeof(MicroBenchmarkConfig))]
public class VectorClockBenchmarks
{
[Params(100, 500, 1000)]
[Params(100)]
public int ClockSize;

[Params(1000)]
Expand Down
111 changes: 56 additions & 55 deletions src/core/Akka.Cluster/VectorClock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is VectorClock && Equals((VectorClock) obj);
return obj is VectorClock vc && Equals(vc);
}

public override int GetHashCode()
Expand Down Expand Up @@ -119,22 +119,26 @@ public static Node FromHash(string hash)
{
return new Node(hash);
}



private static Node Hash(string name)
{
var md5 = System.Security.Cryptography.MD5.Create();
var inputBytes = Encoding.UTF8.GetBytes(name);
var hash = md5.ComputeHash(inputBytes);
// TODO: replace with Murmur3 or SHA512, some other consistent hash algorithm for FIPS complaince and no collisions in Akka.NET v1.5 or 2.0
using(var md5 = System.Security.Cryptography.MD5.Create()){
var inputBytes = Encoding.UTF8.GetBytes(name);
var hash = md5.ComputeHash(inputBytes);


var sb = new StringBuilder();
var sb = new StringBuilder();

foreach (var t in hash)
{
sb.Append(t.ToString("X2"));
}
foreach (var t in hash)
{
sb.Append(t.ToString("X2"));
}

return new Node(sb.ToString());
return new Node(sb.ToString());
}
}

/// <inheritdoc/>
Expand Down Expand Up @@ -167,7 +171,7 @@ public int CompareTo(Node other)
/// <summary>
/// Timestamp used by the <see cref="VectorClock"/>.
/// </summary>
internal class Timestamp
internal static class Timestamp
{
/// <summary>
/// TBD
Expand Down Expand Up @@ -338,7 +342,7 @@ public bool IsSameAs(VectorClock that)
return left.IsConcurrentWith(right);
}

private static readonly KeyValuePair<Node, long> CmpEndMarker = new KeyValuePair<Node, long>(Node.Create("endmarker"), long.MinValue);
private static readonly (Node, long) CmpEndMarker = (Node.Create("endmarker"), Timestamp.EndMarker);

/// <summary>
/// <para>
Expand All @@ -364,58 +368,55 @@ public bool IsSameAs(VectorClock that)
/// <returns>The true ordering based on the contents of the vectorclock.</returns>
internal Ordering CompareOnlyTo(VectorClock that, Ordering order)
{
if (ReferenceEquals(this, that) || Versions.Equals(that.Versions)) return Ordering.Same;
if (ReferenceEquals(this, that) || ReferenceEquals(this.Versions, that.Versions)) return Ordering.Same;

return Compare(_versions.GetEnumerator(), that._versions.GetEnumerator(), order == Ordering.Concurrent ? Ordering.FullOrder : order);
return Compare(_versions.Select(x => (x.Key, x.Value)).GetEnumerator(), that._versions.Select(y => (y.Key, y.Value)).GetEnumerator(), order == Ordering.Concurrent ? Ordering.FullOrder : order);
}

private static Ordering Compare(IEnumerator<KeyValuePair<Node, long>> i1, IEnumerator<KeyValuePair<Node, long>> i2, Ordering requestedOrder)
private static Ordering Compare(IEnumerator<(Node, long)> i1, IEnumerator<(Node, long)> i2, Ordering requestedOrder)
{
//TODO: Tail recursion issues?
Func<KeyValuePair<Node, long>, KeyValuePair<Node, long>, Ordering, Ordering> compareNext = null;
compareNext =
(nt1, nt2, currentOrder) =>
Ordering CompareNext((Node, long) nt1, (Node, long) nt2, Ordering currentOrder)
{
if (requestedOrder != Ordering.FullOrder && currentOrder != Ordering.Same && currentOrder != requestedOrder) return currentOrder;
if (nt1.Equals(CmpEndMarker) && nt2.Equals(CmpEndMarker)) return currentOrder;
// i1 is empty but i2 is not, so i1 can only be Before
if (nt1.Equals(CmpEndMarker)) return currentOrder == Ordering.After ? Ordering.Concurrent : Ordering.Before;
// i2 is empty but i1 is not, so i1 can only be After
if (nt2.Equals(CmpEndMarker)) return currentOrder == Ordering.Before ? Ordering.Concurrent : Ordering.After;
// compare the nodes
var nc = nt1.Item1.CompareTo(nt2.Item1);
if (nc == 0)
{
if (requestedOrder != Ordering.FullOrder && currentOrder != Ordering.Same &&
currentOrder != requestedOrder)
return currentOrder;
if (nt1.Equals(CmpEndMarker) && nt2.Equals(CmpEndMarker)) return currentOrder;
// i1 is empty but i2 is not, so i1 can only be Before
if (nt1.Equals(CmpEndMarker))
return currentOrder == Ordering.After ? Ordering.Concurrent : Ordering.Before;
// i2 is empty but i1 is not, so i1 can only be After
if (nt2.Equals(CmpEndMarker))
return currentOrder == Ordering.Before ? Ordering.Concurrent : Ordering.After;
// compare the nodes
var nc = nt1.Key.CompareTo(nt2.Key);
if (nc == 0)
{
// both nodes exist compare the timestamps
// same timestamp so just continue with the next nodes
if (nt1.Value == nt2.Value)
return compareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), currentOrder);
if (nt1.Value < nt2.Value)
{
// t1 is less than t2, so i1 can only be Before
if (currentOrder == Ordering.After) return Ordering.Concurrent;
return compareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker),
Ordering.Before);
}
if (currentOrder == Ordering.Before) return Ordering.Concurrent;
return compareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), Ordering.After);
}
if (nc < 0)
// both nodes exist compare the timestamps
// same timestamp so just continue with the next nodes
if (nt1.Item2 == nt2.Item2) return CompareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), currentOrder);
if (nt1.Item2 < nt2.Item2)
{
// this node only exists in i1 so i1 can only be After
if (currentOrder == Ordering.Before) return Ordering.Concurrent;
return compareNext(NextOrElse(i1, CmpEndMarker), nt2, Ordering.After);
// t1 is less than t2, so i1 can only be Before
if (currentOrder == Ordering.After) return Ordering.Concurrent;
return CompareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), Ordering.Before);
}
// this node only exists in i2 so i1 can only be Before
if (currentOrder == Ordering.After) return Ordering.Concurrent;
return compareNext(nt1, NextOrElse(i2, CmpEndMarker), Ordering.Before);
};

return compareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), Ordering.Same);
if (currentOrder == Ordering.Before) return Ordering.Concurrent;
return CompareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), Ordering.After);
}

if (nc < 0)
{
// this node only exists in i1 so i1 can only be After
if (currentOrder == Ordering.Before) return Ordering.Concurrent;
return CompareNext(NextOrElse(i1, CmpEndMarker), nt2, Ordering.After);
}

// this node only exists in i2 so i1 can only be Before
if (currentOrder == Ordering.After) return Ordering.Concurrent;
return CompareNext(nt1, NextOrElse(i2, CmpEndMarker), Ordering.Before);
}

using(i1)
using(i2)
return CompareNext(NextOrElse(i1, CmpEndMarker), NextOrElse(i2, CmpEndMarker), Ordering.Same);
}

private static T NextOrElse<T>(IEnumerator<T> iter, T @default)
Expand Down

0 comments on commit 1f81842

Please sign in to comment.