Skip to content

Instantly share code, notes, and snippets.

@tkp1n
Last active June 6, 2020 17:56
Show Gist options
  • Save tkp1n/e9bce5c411f9c5f9c65d64c838342679 to your computer and use it in GitHub Desktop.
Save tkp1n/e9bce5c411f9c5f9c65d64c838342679 to your computer and use it in GitHub Desktop.
Benchmarks: Add methods to convert between hexadecimal strings and bytes
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(); }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment