BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.264 (2004/?/20H1)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.5.20279.10
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.27801, CoreFX 5.0.20.27801), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.27801, CoreFX 5.0.20.27801), X64 RyuJIT
Methods in bold were selected for the PR
Method | NofBytes | Mean | Error | StdDev |
---|---|---|---|---|
EncodeUnsafeWithUint32LookupTables | 16 | 13.78 ns | 0.167 ns | 0.148 ns |
EncodeUnsafeWithCharLookupTable | 16 | 21.99 ns | 0.463 ns | 0.410 ns |
EncodeUnsafeWithLocalCharLookupTable | 16 | 22.60 ns | 0.155 ns | 0.145 ns |
EncodeUnsafeUsingHexConverter_ToString | 16 | 28.22 ns | 0.237 ns | 0.210 ns |
EncodeSafeBitConverter_ToString_like | 16 | 36.93 ns | 0.210 ns | 0.187 ns |
DecodeUnsafeWithByteLookupTable | 16 | 31.94 ns | 0.214 ns | 0.201 ns |
DecodeSafeWithByteLookupTableThrow | 16 | 35.15 ns | 0.196 ns | 0.174 ns |
DecodeSafeWithByteLookupTableReturn | 16 | 39.09 ns | 0.429 ns | 0.358 ns |
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace Hex.Benchmark
{
public unsafe class Program
{
// ENCODING BENCHMARKS
// Unsafe
// Memory: One endian-dependant uint lookup table (2 * 256 * 4 bytes)
// Runtime: One lookup per input byte
[Benchmark]
public void EncodeUnsafeWithUint32LookupTables()
{
fixed (byte* dataPtr = _data)
fixed (char* charPtr = _target)
fixed (uint* lut = BitConverter.IsLittleEndian ? s_hexEncodeLookupLittleEndian : s_hexEncodeLookupBigEndian)
{
byte* inData = dataPtr;
byte* eof = dataPtr + _data.Length;
uint* dest = (uint*) charPtr;
while (eof > inData)
{
*dest++ = lut[*inData++];
}
}
}
// Unsafe
// Memory: One char uint lookup table (16 * 2 bytes)
// Runtime: Two lookups per input byte plus either a SHR or an AND
[Benchmark]
public void EncodeUnsafeWithCharLookupTable()
{
fixed (byte* dataPtr = _data)
fixed (char* charPtr = _target)
fixed (char* lut = s_hexEncodeLookup)
{
byte* inData = dataPtr;
byte* eof = inData + _data.Length;
char* dest = charPtr;
while (eof > inData)
{
int b = *inData++;
*dest++ = lut[b >> 4];
*dest++ = lut[b & 0x0F];
}
}
}
// Unsafe
// Memory: One char uint lookup table (16 * 2 bytes)
// Runtime: Two lookups per input byte plus either a SHR or an AND
[Benchmark]
public void EncodeUnsafeWithLocalCharLookupTable()
{
var lut = "0123456789ABCDEF".AsSpan();
fixed (byte* dataPtr = _data)
fixed (char* charPtr = _target)
{
byte* inData = dataPtr;
byte* eof = inData + _data.Length;
char* dest = charPtr;
while (eof > inData)
{
int b = *inData++;
*dest++ = lut[b >> 4];
*dest++ = lut[b & 0x0F];
}
}
}
// Unsafe
// Memory: No static memory
// Runtime: A bunch of bit-twiddling per input byte
[Benchmark]
public void EncodeUnsafeUsingHexConverter_ToString()
{
fixed (byte* dataPtr = _data)
fixed (char* charPtr = _target)
{
var ros = new ReadOnlySpan<byte>(dataPtr, _data.Length);
var chars = new Span<char>(charPtr, _target.Length);
for (int pos = 0; pos < ros.Length; ++pos)
{
ToCharsBuffer(ros[pos], chars, pos * 2);
}
}
}
// Safe
// Memory: No static memory
// Runtime: A bunch of bit-twiddling per input nibble
[Benchmark]
public void EncodeSafeBitConverter_ToString_like()
{
ReadOnlySpan<byte> src = _data;
Span<char> dst = _target;
int i = 0;
int j = 0;
while (i < src.Length)
{
int b = src[i++];
dst[j++] = ToCharUpper(b >> 4);
dst[j++] = ToCharUpper(b);
}
}
// DECODING BENCHMARKS
[Benchmark]
public void DecodeUnsafeWithByteLookupTable()
{
fixed (char* textPtr = _text)
fixed (byte* dataPtr = _data)
{
char* inChars = textPtr;
byte* outData = dataPtr;
char* eof = inChars + _text.Length;
while (eof > inChars)
{
int valHi = *inChars++;
int valLo = *inChars++;
if (valHi > 0xFF) ThrowBadHexCharFormatException();
if (valLo > 0xFF) ThrowBadHexCharFormatException();
int byteHi = HexDecodeLookup[valHi];
int byteLo = HexDecodeLookup[valLo];
if (byteHi == -1) ThrowBadHexCharFormatException();
if (byteLo == -1) ThrowBadHexCharFormatException();
*outData++ = (byte) ((byteHi << 4) | byteLo);
}
}
}
[Benchmark]
public void DecodeSafeWithByteLookupTableThrow()
{
var src = _text.AsSpan();
var dst = _data.AsSpan();
int i = 0;
int j = 0;
while (j < dst.Length)
{
int charHi = src[i++];
int charLo = src[i++];
if (charHi > 0xFF) ThrowBadHexCharFormatException();
if (charLo > 0xFF) ThrowBadHexCharFormatException();
int byteHi = HexDecodeLookup[charHi];
int byteLo = HexDecodeLookup[charLo];
if (byteHi == -1) ThrowBadHexCharFormatException();
if (byteLo == -1) ThrowBadHexCharFormatException();
dst[j++] = (byte) ((byteHi << 4) | byteLo);
}
}
[Benchmark]
public bool DecodeSafeWithByteLookupTableReturn()
{
var src = _text.AsSpan();
var dst = _data.AsSpan();
int i = 0;
int j = 0;
while (j < dst.Length)
{
int charHi = src[i++];
int charLo = src[i++];
if (charHi > 0xFF) return false;
if (charLo > 0xFF) return false;
int byteHi = HexDecodeLookup[charHi];
int byteLo = HexDecodeLookup[charLo];
if (byteHi == -1) return false;
if (byteLo == -1) return false;
dst[j++] = (byte) ((byteHi << 4) | byteLo);
}
return true;
}
// Infrastructure
private byte[] _data;
private char[] _target;
private string _text;
[Params(16, 2048)]
public int NofBytes { get; set; }
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
_data = new byte[NofBytes];
_target = new char[NofBytes * 2];
random.NextBytes(_data);
_text = BitConverter.ToString(_data).Replace("-", string.Empty);
}
static void Main(string[] args) => BenchmarkRunner.Run<Program>();
/// helper methods and lookup tables only beyond this point
private static ReadOnlySpan<sbyte> HexDecodeLookup => new sbyte[] // rely on C# compiler optimization to reference static data
{
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1,
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};
private static readonly uint[] s_hexEncodeLookupBigEndian =
{
0x00300030, 0x00300031, 0x00300032, 0x00300033, 0x00300034, 0x00300035, 0x00300036, 0x00300037,
0x00300038, 0x00300039, 0x00300041, 0x00300042, 0x00300043, 0x00300044, 0x00300045, 0x00300046,
0x00310030, 0x00310031, 0x00310032, 0x00310033, 0x00310034, 0x00310035, 0x00310036, 0x00310037,
0x00310038, 0x00310039, 0x00310041, 0x00310042, 0x00310043, 0x00310044, 0x00310045, 0x00310046,
0x00320030, 0x00320031, 0x00320032, 0x00320033, 0x00320034, 0x00320035, 0x00320036, 0x00320037,
0x00320038, 0x00320039, 0x00320041, 0x00320042, 0x00320043, 0x00320044, 0x00320045, 0x00320046,
0x00330030, 0x00330031, 0x00330032, 0x00330033, 0x00330034, 0x00330035, 0x00330036, 0x00330037,
0x00330038, 0x00330039, 0x00330041, 0x00330042, 0x00330043, 0x00330044, 0x00330045, 0x00330046,
0x00340030, 0x00340031, 0x00340032, 0x00340033, 0x00340034, 0x00340035, 0x00340036, 0x00340037,
0x00340038, 0x00340039, 0x00340041, 0x00340042, 0x00340043, 0x00340044, 0x00340045, 0x00340046,
0x00350030, 0x00350031, 0x00350032, 0x00350033, 0x00350034, 0x00350035, 0x00350036, 0x00350037,
0x00350038, 0x00350039, 0x00350041, 0x00350042, 0x00350043, 0x00350044, 0x00350045, 0x00350046,
0x00360030, 0x00360031, 0x00360032, 0x00360033, 0x00360034, 0x00360035, 0x00360036, 0x00360037,
0x00360038, 0x00360039, 0x00360041, 0x00360042, 0x00360043, 0x00360044, 0x00360045, 0x00360046,
0x00370030, 0x00370031, 0x00370032, 0x00370033, 0x00370034, 0x00370035, 0x00370036, 0x00370037,
0x00370038, 0x00370039, 0x00370041, 0x00370042, 0x00370043, 0x00370044, 0x00370045, 0x00370046,
0x00380030, 0x00380031, 0x00380032, 0x00380033, 0x00380034, 0x00380035, 0x00380036, 0x00380037,
0x00380038, 0x00380039, 0x00380041, 0x00380042, 0x00380043, 0x00380044, 0x00380045, 0x00380046,
0x00390030, 0x00390031, 0x00390032, 0x00390033, 0x00390034, 0x00390035, 0x00390036, 0x00390037,
0x00390038, 0x00390039, 0x00390041, 0x00390042, 0x00390043, 0x00390044, 0x00390045, 0x00390046,
0x00410030, 0x00410031, 0x00410032, 0x00410033, 0x00410034, 0x00410035, 0x00410036, 0x00410037,
0x00410038, 0x00410039, 0x00410041, 0x00410042, 0x00410043, 0x00410044, 0x00410045, 0x00410046,
0x00420030, 0x00420031, 0x00420032, 0x00420033, 0x00420034, 0x00420035, 0x00420036, 0x00420037,
0x00420038, 0x00420039, 0x00420041, 0x00420042, 0x00420043, 0x00420044, 0x00420045, 0x00420046,
0x00430030, 0x00430031, 0x00430032, 0x00430033, 0x00430034, 0x00430035, 0x00430036, 0x00430037,
0x00430038, 0x00430039, 0x00430041, 0x00430042, 0x00430043, 0x00430044, 0x00430045, 0x00430046,
0x00440030, 0x00440031, 0x00440032, 0x00440033, 0x00440034, 0x00440035, 0x00440036, 0x00440037,
0x00440038, 0x00440039, 0x00440041, 0x00440042, 0x00440043, 0x00440044, 0x00440045, 0x00440046,
0x00450030, 0x00450031, 0x00450032, 0x00450033, 0x00450034, 0x00450035, 0x00450036, 0x00450037,
0x00450038, 0x00450039, 0x00450041, 0x00450042, 0x00450043, 0x00450044, 0x00450045, 0x00450046,
0x00460030, 0x00460031, 0x00460032, 0x00460033, 0x00460034, 0x00460035, 0x00460036, 0x00460037,
0x00460038, 0x00460039, 0x00460041, 0x00460042, 0x00460043, 0x00460044, 0x00460045, 0x00460046
};
private static readonly uint[] s_hexEncodeLookupLittleEndian =
{
0x00300030, 0x00310030, 0x00320030, 0x00330030, 0x00340030, 0x00350030, 0x00360030, 0x00370030,
0x00380030, 0x00390030, 0x00410030, 0x00420030, 0x00430030, 0x00440030, 0x00450030, 0x00460030,
0x00300031, 0x00310031, 0x00320031, 0x00330031, 0x00340031, 0x00350031, 0x00360031, 0x00370031,
0x00380031, 0x00390031, 0x00410031, 0x00420031, 0x00430031, 0x00440031, 0x00450031, 0x00460031,
0x00300032, 0x00310032, 0x00320032, 0x00330032, 0x00340032, 0x00350032, 0x00360032, 0x00370032,
0x00380032, 0x00390032, 0x00410032, 0x00420032, 0x00430032, 0x00440032, 0x00450032, 0x00460032,
0x00300033, 0x00310033, 0x00320033, 0x00330033, 0x00340033, 0x00350033, 0x00360033, 0x00370033,
0x00380033, 0x00390033, 0x00410033, 0x00420033, 0x00430033, 0x00440033, 0x00450033, 0x00460033,
0x00300034, 0x00310034, 0x00320034, 0x00330034, 0x00340034, 0x00350034, 0x00360034, 0x00370034,
0x00380034, 0x00390034, 0x00410034, 0x00420034, 0x00430034, 0x00440034, 0x00450034, 0x00460034,
0x00300035, 0x00310035, 0x00320035, 0x00330035, 0x00340035, 0x00350035, 0x00360035, 0x00370035,
0x00380035, 0x00390035, 0x00410035, 0x00420035, 0x00430035, 0x00440035, 0x00450035, 0x00460035,
0x00300036, 0x00310036, 0x00320036, 0x00330036, 0x00340036, 0x00350036, 0x00360036, 0x00370036,
0x00380036, 0x00390036, 0x00410036, 0x00420036, 0x00430036, 0x00440036, 0x00450036, 0x00460036,
0x00300037, 0x00310037, 0x00320037, 0x00330037, 0x00340037, 0x00350037, 0x00360037, 0x00370037,
0x00380037, 0x00390037, 0x00410037, 0x00420037, 0x00430037, 0x00440037, 0x00450037, 0x00460037,
0x00300038, 0x00310038, 0x00320038, 0x00330038, 0x00340038, 0x00350038, 0x00360038, 0x00370038,
0x00380038, 0x00390038, 0x00410038, 0x00420038, 0x00430038, 0x00440038, 0x00450038, 0x00460038,
0x00300039, 0x00310039, 0x00320039, 0x00330039, 0x00340039, 0x00350039, 0x00360039, 0x00370039,
0x00380039, 0x00390039, 0x00410039, 0x00420039, 0x00430039, 0x00440039, 0x00450039, 0x00460039,
0x00300041, 0x00310041, 0x00320041, 0x00330041, 0x00340041, 0x00350041, 0x00360041, 0x00370041,
0x00380041, 0x00390041, 0x00410041, 0x00420041, 0x00430041, 0x00440041, 0x00450041, 0x00460041,
0x00300042, 0x00310042, 0x00320042, 0x00330042, 0x00340042, 0x00350042, 0x00360042, 0x00370042,
0x00380042, 0x00390042, 0x00410042, 0x00420042, 0x00430042, 0x00440042, 0x00450042, 0x00460042,
0x00300043, 0x00310043, 0x00320043, 0x00330043, 0x00340043, 0x00350043, 0x00360043, 0x00370043,
0x00380043, 0x00390043, 0x00410043, 0x00420043, 0x00430043, 0x00440043, 0x00450043, 0x00460043,
0x00300044, 0x00310044, 0x00320044, 0x00330044, 0x00340044, 0x00350044, 0x00360044, 0x00370044,
0x00380044, 0x00390044, 0x00410044, 0x00420044, 0x00430044, 0x00440044, 0x00450044, 0x00460044,
0x00300045, 0x00310045, 0x00320045, 0x00330045, 0x00340045, 0x00350045, 0x00360045, 0x00370045,
0x00380045, 0x00390045, 0x00410045, 0x00420045, 0x00430045, 0x00440045, 0x00450045, 0x00460045,
0x00300046, 0x00310046, 0x00320046, 0x00330046, 0x00340046, 0x00350046, 0x00360046, 0x00370046,
0x00380046, 0x00390046, 0x00410046, 0x00420046, 0x00430046, 0x00440046, 0x00450046, 0x00460046
};
private static readonly char[] s_hexEncodeLookup =
{
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
{
uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
buffer[startingIndex] = (char)(packedResult >> 8);
}
public enum Casing : uint
{
// Output [ '0' .. '9' ] and [ 'A' .. 'F' ].
Upper = 0,
// Output [ '0' .. '9' ] and [ 'a' .. 'f' ].
// This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ])
// already have the 0x20 bit set, so ORing them with 0x20 is a no-op,
// while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ])
// don't have the 0x20 bit set, so ORing them maps to
// [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want.
Lower = 0x2020U,
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static char ToCharUpper(int value)
{
value &= 0xF;
value += '0';
if (value > '9')
{
value += ('A' - ('9' + 1));
}
return (char)value;
}
[DoesNotReturn]
private static void ThrowBadHexCharFormatException() { throw new FormatException(); }
}
}