Particular / NServiceBus.RabbitMQ

RabbitMQ transport for NServiceBus
https://docs.particular.net/nservicebus/rabbitmq/
Other
88 stars 58 forks source link

Optimize the delay delivery routing key calculation for throughput and fewer allocations #1411

Closed danielmarbach closed 2 months ago

danielmarbach commented 2 months ago

Based upon #1412

This PR optimizes the delay delivery routing key algorithm to:

The routing key length is well-defined by the number of levels we allow plus the space needed for the address. This means we can use string.Create to get rid of the StringBuilder.

As a replacement for the BitArray the BitVector32 is a nice fit which is a struct.

To avoid having to loop through the bit vector multiple times or encoding some custom string parts into the string created to then splice the delayLevel out again I had to do a bit of more advanced trickery to be able to write the delayLevel which requires some unsafe declaration. That is fine though and even something the underlying RabbitMQ client uses.

image

## Benchmark ```csharp using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Runtime.CompilerServices; using System.Text; using BenchmarkDotNet.Attributes; namespace MicroBenchmarks.RabbitMQ; [SimpleJob] [MemoryDiagnoser] public class RoutingKey { [Benchmark(Baseline = true)] [ArgumentsSource(nameof(Arguments))] public string Before(int delay, string address) => CalculateRoutingKeyOld(10, "some-address", out _); [Benchmark] [ArgumentsSource(nameof(Arguments))] public string After(int delay, string address) => CalculateRoutingKeyNew(10, "some-address", out _); public IEnumerable Arguments() { yield return [0, "some-address"]; yield return [10, "some-address"]; yield return [MaxDelayInSeconds - 1, "almost-max"]; yield return [MaxDelayInSeconds, "max"]; } public static string CalculateRoutingKeyOld(int delayInSeconds, string address, out int startingDelayLevel) { if (delayInSeconds < 0) { delayInSeconds = 0; } var bitArray = new BitArray(new[] { delayInSeconds }); var sb = new StringBuilder(); startingDelayLevel = 0; for (var level = MaxLevel; level >= 0; level--) { if (startingDelayLevel == 0 && bitArray[level]) { startingDelayLevel = level; } sb.Append(bitArray[level] ? "1." : "0."); } sb.Append(address); return sb.ToString(); } public static unsafe string CalculateRoutingKeyNew(int delayInSeconds, string address, out int startingDelayLevel) { if (delayInSeconds < 0) { delayInSeconds = 0; } startingDelayLevel = 0; fixed (int* startingDelayLevelPtr = &startingDelayLevel) { var addr = (IntPtr)startingDelayLevelPtr; return string.Create((2 * MaxLevel) + 2 + address.Length, (address, delayInSeconds, addr), Action); static void Action(Span span, (string address, int, IntPtr) state) { var (address, delayInSeconds, startingDelayLevelPtr) = state; var startingDelayLevel = 0; var mask = BitVector32.CreateMask(); var bitVector = new BitVector32(delayInSeconds); var index = 0; for (var level = MaxLevel; level >= 0; level--) { var flag = bitVector[mask << level]; if (startingDelayLevel == 0 && flag) { startingDelayLevel = level; } span[index++] = flag ? '1' : '0'; span[index++] = '.'; } address.AsSpan().CopyTo(span[index..]); Unsafe.Write(startingDelayLevelPtr.ToPointer(), startingDelayLevel); } } } const int maxNumberOfBitsToUse = 28; public const int MaxLevel = maxNumberOfBitsToUse - 1; public const int MaxDelayInSeconds = (1 << maxNumberOfBitsToUse) - 1; } ```