Skip to content

Instantly share code, notes, and snippets.

@ladeak
Created August 20, 2024 20:02
Show Gist options
  • Save ladeak/b410b361d45549c64548912e8971da24 to your computer and use it in GitHub Desktop.
Save ladeak/b410b361d45549c64548912e8971da24 to your computer and use it in GitHub Desktop.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<Benchmarks>();
[SimpleJob, MemoryDiagnoser, DisassemblyDiagnoser]
public class Benchmarks
{
private byte[] _buffer = new byte[32];
[Params(6, 17, 123)]
public ulong Value { get; set; }
[Benchmark]
public void TestUnsafe()
{
WriteNumericUnsafe(_buffer, Value);
}
[Benchmark]
public void TestSpan()
{
WriteNumericSpan(_buffer, Value);
}
[Benchmark]
public void TestRef()
{
WriteNumericRef(_buffer, Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void WriteNumericUnsafe(Span<byte> buffer, ulong number)
{
const byte AsciiDigitStart = (byte)'0';
var span = buffer;
int position = 0;
var bytesLeftInBlock = span.Length;
// Fast path, try copying to the available memory directly
fixed (byte* output = span)
{
var start = output;
if (number < 10 && bytesLeftInBlock >= 1)
{
*(start) = (byte)(((uint)number) + AsciiDigitStart);
position++;
}
else if (number < 100 && bytesLeftInBlock >= 2)
{
var val = (uint)number;
var tens = (byte)((val * 205u) >> 11); // div10, valid to 1028
*(start) = (byte)(tens + AsciiDigitStart);
*(start + 1) = (byte)(val - (tens * 10) + AsciiDigitStart);
position += 2;
}
else if (number < 1000 && bytesLeftInBlock >= 3)
{
var val = (uint)number;
var digit0 = (byte)((val * 41u) >> 12); // div100, valid to 1098
var digits01 = (byte)((val * 205u) >> 11); // div10, valid to 1028
*(start) = (byte)(digit0 + AsciiDigitStart);
*(start + 1) = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
*(start + 2) = (byte)(val - (digits01 * 10) + AsciiDigitStart);
position += 3;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void WriteNumericSpan(Span<byte> buffer, ulong number)
{
const byte AsciiDigitStart = (byte)'0';
var span = buffer;
int position = 0;
var bytesLeftInBlock = span.Length;
// Fast path, try copying to the available memory directly
var start = span;
if (number < 10 && bytesLeftInBlock >= 1)
{
start[0] = (byte)(((uint)number) + AsciiDigitStart);
position++;
}
else if (number < 100 && bytesLeftInBlock >= 2)
{
var val = (uint)number;
var tens = (byte)((val * 205u) >> 11); // div10, valid to 1028
start[0] = (byte)(tens + AsciiDigitStart);
start[1] = (byte)(val - (tens * 10) + AsciiDigitStart);
position += 2;
}
else if (number < 1000 && bytesLeftInBlock >= 3)
{
var val = (uint)number;
var digit0 = (byte)((val * 41u) >> 12); // div100, valid to 1098
var digits01 = (byte)((val * 205u) >> 11); // div10, valid to 1028
start[0] = (byte)(digit0 + AsciiDigitStart);
start[1] = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
start[2] = (byte)(val - (digits01 * 10) + AsciiDigitStart);
position += 3;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void WriteNumericRef(Span<byte> buffer, ulong number)
{
const byte AsciiDigitStart = (byte)'0';
var span = buffer;
int position = 0;
var bytesLeftInBlock = span.Length;
// Fast path, try copying to the available memory directly
ref byte start = ref MemoryMarshal.GetReference(span);
if (number < 10 && bytesLeftInBlock >= 1)
{
start = (byte)(((uint)number) + AsciiDigitStart);
position++;
}
else if (number < 100 && bytesLeftInBlock >= 2)
{
var val = (uint)number;
var tens = (byte)((val * 205u) >> 11); // div10, valid to 1028
start = (byte)(tens + AsciiDigitStart);
Unsafe.AddByteOffset(ref start, 1) = (byte)(val - (tens * 10) + AsciiDigitStart);
position += 2;
}
else if (number < 1000 && bytesLeftInBlock >= 3)
{
var val = (uint)number;
var digit0 = (byte)((val * 41u) >> 12); // div100, valid to 1098
var digits01 = (byte)((val * 205u) >> 11); // div10, valid to 1028
start = (byte)(digit0 + AsciiDigitStart);
Unsafe.AddByteOffset(ref start, 1) = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
Unsafe.AddByteOffset(ref start, 2) = (byte)(val - (digits01 * 10) + AsciiDigitStart);
position += 3;
}
}
}
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4037/23H2/2023Update/SunValley3)
12th Gen Intel Core i7-1255U, 1 CPU, 12 logical and 10 physical cores
.NET SDK 9.0.100-preview.7.24407.12
  [Host]     : .NET 9.0.0 (9.0.24.40507), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.0 (9.0.24.40507), X64 RyuJIT AVX2


| Method     | Value | Mean      | Error     | StdDev    | Code Size | Allocated |
|----------- |------ |----------:|----------:|----------:|----------:|----------:|
| TestUnsafe | 6     | 0.1193 ns | 0.0160 ns | 0.0133 ns |     215 B |         - |
| TestSpan   | 6     | 0.1114 ns | 0.0105 ns | 0.0093 ns |     186 B |         - |
| TestRef    | 6     | 0.1130 ns | 0.0133 ns | 0.0125 ns |     186 B |         - |
| TestUnsafe | 17    | 0.3625 ns | 0.0168 ns | 0.0149 ns |     212 B |         - |
| TestSpan   | 17    | 0.1063 ns | 0.0090 ns | 0.0075 ns |     183 B |         - |
| TestRef    | 17    | 0.1078 ns | 0.0091 ns | 0.0081 ns |     183 B |         - |
| TestUnsafe | 123   | 0.6305 ns | 0.0146 ns | 0.0137 ns |     218 B |         - |
| TestSpan   | 123   | 0.3738 ns | 0.0116 ns | 0.0103 ns |     181 B |         - |
| TestRef    | 123   | 0.3548 ns | 0.0087 ns | 0.0077 ns |     181 B |         - |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment