Skip to content

Instantly share code, notes, and snippets.

@antoninkriz
Last active February 11, 2024 08:38
Show Gist options
  • Save antoninkriz/915364de7f264dd14a572936abd5228b to your computer and use it in GitHub Desktop.
Save antoninkriz/915364de7f264dd14a572936abd5228b to your computer and use it in GitHub Desktop.
C# - Performance analysis of converting byte[] to hexadecimal string
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace Benchmark;
public class Bench
{
[Params(10, 100, 500, 1000, 10000, 1000000)]
public int N;
#region PrecomputedMappings
private const string HexAlphabetString = "0123456789ABCDEF";
private static readonly char[] HexAlphabetArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private static ReadOnlySpan<byte> HexAlphabetSpan => new[]
{
(byte)'0',
(byte)'1',
(byte)'2',
(byte)'3',
(byte)'4',
(byte)'5',
(byte)'6',
(byte)'7',
(byte)'8',
(byte)'9',
(byte)'A',
(byte)'B',
(byte)'C',
(byte)'D',
(byte)'E',
(byte)'F',
};
// { "00", "01", ..., "0E", "0F", "10", "11", ..., "FE", "FF" }
private static readonly string[] HexStringTable =
HexAlphabetString.SelectMany(n1 => HexAlphabetString.Select(n2 => new string(new[] { n1, n2 }))).ToArray();
private static readonly uint[] Lookup32 = Enumerable.Range(0, 256).Select(i =>
{
string s = i.ToString("X2");
return s[0] + ((uint)s[1] << 16);
}).ToArray();
private static readonly unsafe uint* Lookup32UnsafeP = (uint*)GCHandle.Alloc(Lookup32, GCHandleType.Pinned).AddrOfPinnedObject();
private static ReadOnlySpan<byte> Lookup32Span => new byte[]
{
0x0, 0x30, 0x0, 0x30,
0x0, 0x31, 0x0, 0x30,
0x0, 0x32, 0x0, 0x30,
0x0, 0x33, 0x0, 0x30,
0x0, 0x34, 0x0, 0x30,
0x0, 0x35, 0x0, 0x30,
0x0, 0x36, 0x0, 0x30,
0x0, 0x37, 0x0, 0x30,
0x0, 0x38, 0x0, 0x30,
0x0, 0x39, 0x0, 0x30,
0x0, 0x61, 0x0, 0x30,
0x0, 0x62, 0x0, 0x30,
0x0, 0x63, 0x0, 0x30,
0x0, 0x64, 0x0, 0x30,
0x0, 0x65, 0x0, 0x30,
0x0, 0x66, 0x0, 0x30,
0x0, 0x30, 0x0, 0x31,
0x0, 0x31, 0x0, 0x31,
0x0, 0x32, 0x0, 0x31,
0x0, 0x33, 0x0, 0x31,
0x0, 0x34, 0x0, 0x31,
0x0, 0x35, 0x0, 0x31,
0x0, 0x36, 0x0, 0x31,
0x0, 0x37, 0x0, 0x31,
0x0, 0x38, 0x0, 0x31,
0x0, 0x39, 0x0, 0x31,
0x0, 0x61, 0x0, 0x31,
0x0, 0x62, 0x0, 0x31,
0x0, 0x63, 0x0, 0x31,
0x0, 0x64, 0x0, 0x31,
0x0, 0x65, 0x0, 0x31,
0x0, 0x66, 0x0, 0x31,
0x0, 0x30, 0x0, 0x32,
0x0, 0x31, 0x0, 0x32,
0x0, 0x32, 0x0, 0x32,
0x0, 0x33, 0x0, 0x32,
0x0, 0x34, 0x0, 0x32,
0x0, 0x35, 0x0, 0x32,
0x0, 0x36, 0x0, 0x32,
0x0, 0x37, 0x0, 0x32,
0x0, 0x38, 0x0, 0x32,
0x0, 0x39, 0x0, 0x32,
0x0, 0x61, 0x0, 0x32,
0x0, 0x62, 0x0, 0x32,
0x0, 0x63, 0x0, 0x32,
0x0, 0x64, 0x0, 0x32,
0x0, 0x65, 0x0, 0x32,
0x0, 0x66, 0x0, 0x32,
0x0, 0x30, 0x0, 0x33,
0x0, 0x31, 0x0, 0x33,
0x0, 0x32, 0x0, 0x33,
0x0, 0x33, 0x0, 0x33,
0x0, 0x34, 0x0, 0x33,
0x0, 0x35, 0x0, 0x33,
0x0, 0x36, 0x0, 0x33,
0x0, 0x37, 0x0, 0x33,
0x0, 0x38, 0x0, 0x33,
0x0, 0x39, 0x0, 0x33,
0x0, 0x61, 0x0, 0x33,
0x0, 0x62, 0x0, 0x33,
0x0, 0x63, 0x0, 0x33,
0x0, 0x64, 0x0, 0x33,
0x0, 0x65, 0x0, 0x33,
0x0, 0x66, 0x0, 0x33,
0x0, 0x30, 0x0, 0x34,
0x0, 0x31, 0x0, 0x34,
0x0, 0x32, 0x0, 0x34,
0x0, 0x33, 0x0, 0x34,
0x0, 0x34, 0x0, 0x34,
0x0, 0x35, 0x0, 0x34,
0x0, 0x36, 0x0, 0x34,
0x0, 0x37, 0x0, 0x34,
0x0, 0x38, 0x0, 0x34,
0x0, 0x39, 0x0, 0x34,
0x0, 0x61, 0x0, 0x34,
0x0, 0x62, 0x0, 0x34,
0x0, 0x63, 0x0, 0x34,
0x0, 0x64, 0x0, 0x34,
0x0, 0x65, 0x0, 0x34,
0x0, 0x66, 0x0, 0x34,
0x0, 0x30, 0x0, 0x35,
0x0, 0x31, 0x0, 0x35,
0x0, 0x32, 0x0, 0x35,
0x0, 0x33, 0x0, 0x35,
0x0, 0x34, 0x0, 0x35,
0x0, 0x35, 0x0, 0x35,
0x0, 0x36, 0x0, 0x35,
0x0, 0x37, 0x0, 0x35,
0x0, 0x38, 0x0, 0x35,
0x0, 0x39, 0x0, 0x35,
0x0, 0x61, 0x0, 0x35,
0x0, 0x62, 0x0, 0x35,
0x0, 0x63, 0x0, 0x35,
0x0, 0x64, 0x0, 0x35,
0x0, 0x65, 0x0, 0x35,
0x0, 0x66, 0x0, 0x35,
0x0, 0x30, 0x0, 0x36,
0x0, 0x31, 0x0, 0x36,
0x0, 0x32, 0x0, 0x36,
0x0, 0x33, 0x0, 0x36,
0x0, 0x34, 0x0, 0x36,
0x0, 0x35, 0x0, 0x36,
0x0, 0x36, 0x0, 0x36,
0x0, 0x37, 0x0, 0x36,
0x0, 0x38, 0x0, 0x36,
0x0, 0x39, 0x0, 0x36,
0x0, 0x61, 0x0, 0x36,
0x0, 0x62, 0x0, 0x36,
0x0, 0x63, 0x0, 0x36,
0x0, 0x64, 0x0, 0x36,
0x0, 0x65, 0x0, 0x36,
0x0, 0x66, 0x0, 0x36,
0x0, 0x30, 0x0, 0x37,
0x0, 0x31, 0x0, 0x37,
0x0, 0x32, 0x0, 0x37,
0x0, 0x33, 0x0, 0x37,
0x0, 0x34, 0x0, 0x37,
0x0, 0x35, 0x0, 0x37,
0x0, 0x36, 0x0, 0x37,
0x0, 0x37, 0x0, 0x37,
0x0, 0x38, 0x0, 0x37,
0x0, 0x39, 0x0, 0x37,
0x0, 0x61, 0x0, 0x37,
0x0, 0x62, 0x0, 0x37,
0x0, 0x63, 0x0, 0x37,
0x0, 0x64, 0x0, 0x37,
0x0, 0x65, 0x0, 0x37,
0x0, 0x66, 0x0, 0x37,
0x0, 0x30, 0x0, 0x38,
0x0, 0x31, 0x0, 0x38,
0x0, 0x32, 0x0, 0x38,
0x0, 0x33, 0x0, 0x38,
0x0, 0x34, 0x0, 0x38,
0x0, 0x35, 0x0, 0x38,
0x0, 0x36, 0x0, 0x38,
0x0, 0x37, 0x0, 0x38,
0x0, 0x38, 0x0, 0x38,
0x0, 0x39, 0x0, 0x38,
0x0, 0x61, 0x0, 0x38,
0x0, 0x62, 0x0, 0x38,
0x0, 0x63, 0x0, 0x38,
0x0, 0x64, 0x0, 0x38,
0x0, 0x65, 0x0, 0x38,
0x0, 0x66, 0x0, 0x38,
0x0, 0x30, 0x0, 0x39,
0x0, 0x31, 0x0, 0x39,
0x0, 0x32, 0x0, 0x39,
0x0, 0x33, 0x0, 0x39,
0x0, 0x34, 0x0, 0x39,
0x0, 0x35, 0x0, 0x39,
0x0, 0x36, 0x0, 0x39,
0x0, 0x37, 0x0, 0x39,
0x0, 0x38, 0x0, 0x39,
0x0, 0x39, 0x0, 0x39,
0x0, 0x61, 0x0, 0x39,
0x0, 0x62, 0x0, 0x39,
0x0, 0x63, 0x0, 0x39,
0x0, 0x64, 0x0, 0x39,
0x0, 0x65, 0x0, 0x39,
0x0, 0x66, 0x0, 0x39,
0x0, 0x30, 0x0, 0x61,
0x0, 0x31, 0x0, 0x61,
0x0, 0x32, 0x0, 0x61,
0x0, 0x33, 0x0, 0x61,
0x0, 0x34, 0x0, 0x61,
0x0, 0x35, 0x0, 0x61,
0x0, 0x36, 0x0, 0x61,
0x0, 0x37, 0x0, 0x61,
0x0, 0x38, 0x0, 0x61,
0x0, 0x39, 0x0, 0x61,
0x0, 0x61, 0x0, 0x61,
0x0, 0x62, 0x0, 0x61,
0x0, 0x63, 0x0, 0x61,
0x0, 0x64, 0x0, 0x61,
0x0, 0x65, 0x0, 0x61,
0x0, 0x66, 0x0, 0x61,
0x0, 0x30, 0x0, 0x62,
0x0, 0x31, 0x0, 0x62,
0x0, 0x32, 0x0, 0x62,
0x0, 0x33, 0x0, 0x62,
0x0, 0x34, 0x0, 0x62,
0x0, 0x35, 0x0, 0x62,
0x0, 0x36, 0x0, 0x62,
0x0, 0x37, 0x0, 0x62,
0x0, 0x38, 0x0, 0x62,
0x0, 0x39, 0x0, 0x62,
0x0, 0x61, 0x0, 0x62,
0x0, 0x62, 0x0, 0x62,
0x0, 0x63, 0x0, 0x62,
0x0, 0x64, 0x0, 0x62,
0x0, 0x65, 0x0, 0x62,
0x0, 0x66, 0x0, 0x62,
0x0, 0x30, 0x0, 0x63,
0x0, 0x31, 0x0, 0x63,
0x0, 0x32, 0x0, 0x63,
0x0, 0x33, 0x0, 0x63,
0x0, 0x34, 0x0, 0x63,
0x0, 0x35, 0x0, 0x63,
0x0, 0x36, 0x0, 0x63,
0x0, 0x37, 0x0, 0x63,
0x0, 0x38, 0x0, 0x63,
0x0, 0x39, 0x0, 0x63,
0x0, 0x61, 0x0, 0x63,
0x0, 0x62, 0x0, 0x63,
0x0, 0x63, 0x0, 0x63,
0x0, 0x64, 0x0, 0x63,
0x0, 0x65, 0x0, 0x63,
0x0, 0x66, 0x0, 0x63,
0x0, 0x30, 0x0, 0x64,
0x0, 0x31, 0x0, 0x64,
0x0, 0x32, 0x0, 0x64,
0x0, 0x33, 0x0, 0x64,
0x0, 0x34, 0x0, 0x64,
0x0, 0x35, 0x0, 0x64,
0x0, 0x36, 0x0, 0x64,
0x0, 0x37, 0x0, 0x64,
0x0, 0x38, 0x0, 0x64,
0x0, 0x39, 0x0, 0x64,
0x0, 0x61, 0x0, 0x64,
0x0, 0x62, 0x0, 0x64,
0x0, 0x63, 0x0, 0x64,
0x0, 0x64, 0x0, 0x64,
0x0, 0x65, 0x0, 0x64,
0x0, 0x66, 0x0, 0x64,
0x0, 0x30, 0x0, 0x65,
0x0, 0x31, 0x0, 0x65,
0x0, 0x32, 0x0, 0x65,
0x0, 0x33, 0x0, 0x65,
0x0, 0x34, 0x0, 0x65,
0x0, 0x35, 0x0, 0x65,
0x0, 0x36, 0x0, 0x65,
0x0, 0x37, 0x0, 0x65,
0x0, 0x38, 0x0, 0x65,
0x0, 0x39, 0x0, 0x65,
0x0, 0x61, 0x0, 0x65,
0x0, 0x62, 0x0, 0x65,
0x0, 0x63, 0x0, 0x65,
0x0, 0x64, 0x0, 0x65,
0x0, 0x65, 0x0, 0x65,
0x0, 0x66, 0x0, 0x65,
0x0, 0x30, 0x0, 0x66,
0x0, 0x31, 0x0, 0x66,
0x0, 0x32, 0x0, 0x66,
0x0, 0x33, 0x0, 0x66,
0x0, 0x34, 0x0, 0x66,
0x0, 0x35, 0x0, 0x66,
0x0, 0x36, 0x0, 0x66,
0x0, 0x37, 0x0, 0x66,
0x0, 0x38, 0x0, 0x66,
0x0, 0x39, 0x0, 0x66,
0x0, 0x61, 0x0, 0x66,
0x0, 0x62, 0x0, 0x66,
0x0, 0x63, 0x0, 0x66,
0x0, 0x64, 0x0, 0x66,
0x0, 0x65, 0x0, 0x66,
0x0, 0x66, 0x0, 0x66
};
#endregion
#region Init
private byte[] _bytes;
public Bench() {
_bytes = new byte[] {0};
}
[GlobalSetup]
public void GlobalSetup()
{
_bytes = new byte[N];
new Random(42).NextBytes(_bytes);
}
#endregion
/// <summary>
/// https://github.com/fit-ctu-discord/honza-botner/blob/45d37346576d254a5acb82dedafd7d6712c1c619/src/HonzaBotner.Services/Sha256HashService.cs
/// by https://github.com/ostorc
/// </summary>
[Benchmark(Baseline = true)]
public string StringBuilderForEachByte()
{
StringBuilder strB = new();
foreach (byte b in _bytes)
strB.Append(b.ToString("X2"));
return strB.ToString();
}
/// <summary>
/// https://stackoverflow.com/a/3824807/3161322
/// by https://stackoverflow.com/users/64084
/// </summary>
[Benchmark]
public string StringBuilderForEachBytePreAllocated()
{
StringBuilder hex = new StringBuilder(_bytes.Length * 2);
foreach (byte b in _bytes)
hex.Append(b.ToString("X2"));
return hex.ToString();
}
/// <summary>
/// unknown, probably modified
/// https://stackoverflow.com/a/3824807/3161322
/// by https://stackoverflow.com/users/64084
/// </summary>
[Benchmark]
public string StringBuilderForEachAppendFormat()
{
StringBuilder hex = new StringBuilder(_bytes.Length * 2);
foreach (byte b in _bytes)
hex.AppendFormat("{0:X2}", b);
return hex.ToString();
}
/// <summary>
/// https://stackoverflow.com/a/311179/3161322
/// by https://stackoverflow.com/users/18771/tomalak
/// </summary>
[Benchmark]
public string BitConverterReplace()
{
string hex = BitConverter.ToString(_bytes);
return hex.Replace("-", "");
}
/// <summary>
/// https://stackoverflow.com/a/311382/3161322
/// by https://stackoverflow.com/users/987/will-dean
/// </summary>
[Benchmark]
public string StringJoinArrayConvertAll()
=> string.Join(string.Empty, Array.ConvertAll(_bytes, b => b.ToString("X2")));
/// <summary>
/// https://stackoverflow.com/a/2345722/3161322
/// by
/// </summary>
[Benchmark]
public string StringJoinSelect()
=> string.Join(string.Empty, _bytes.Select(bin => bin.ToString("X2")).ToArray());
/// <summary>
/// https://stackoverflow.com/a/311382/3161322
/// by https://stackoverflow.com/users/987/will-dean
/// </summary>
[Benchmark]
public string StringConcatArrayConvertAll()
=> string.Concat(Array.ConvertAll(_bytes, b => b.ToString("X2")));
/// <summary>
/// https://stackoverflow.com/a/311382/3161322
/// by https://stackoverflow.com/users/149265/allon-guralnek
/// </summary>
[Benchmark]
public string StringConcatSelect()
=> string.Concat(_bytes.Select(b => b.ToString("X2")));
/// <summary>
/// https://stackoverflow.com/a/3824807/3161322
/// by https://stackoverflow.com/users/64084
/// </summary>
[Benchmark]
public string StringBuilderAggregateBytesAppend()
=> _bytes.Aggregate(new StringBuilder(_bytes.Length * 2), (sb, b) => sb.Append(b.ToString("X2"))).ToString();
/// <summary>
/// unknown, probably modified
/// https://stackoverflow.com/a/3824807/3161322
/// by https://stackoverflow.com/users/64084
/// </summary>
[Benchmark]
public string StringBuilderAggregateBytesAppendFormat()
=> _bytes.Aggregate(new StringBuilder(_bytes.Length * 2), (sb, b) => sb.AppendFormat("{0:X2}", b)).ToString();
/// <summary>
/// https://stackoverflow.com/a/632920/3161322
/// by https://stackoverflow.com/users/676066
/// </summary>
[Benchmark]
public string ByteManipulationHexMultiply()
{
char[] c = new char[_bytes.Length * 2];
byte b;
for (int i = 0; i < _bytes.Length; i++)
{
b = ((byte)(_bytes[i] >> 4));
c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
b = ((byte)(_bytes[i] & 0xF));
c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
}
return new string(c);
}
/// <summary>
/// https://stackoverflow.com/a/3974535/3161322
/// by https://stackoverflow.com/users/21784/kgriffs
/// </summary>
[Benchmark]
public string ByteManipulationHexIncrement()
{
char[] c = new char[_bytes.Length * 2];
byte b;
for (int bx = 0, cx = 0; bx < _bytes.Length; ++bx, ++cx)
{
b = ((byte)(_bytes[bx] >> 4));
c[cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
b = ((byte)(_bytes[bx] & 0x0F));
c[++cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
}
return new string(c);
}
/// <summary>
/// https://stackoverflow.com/a/14333437/3161322
/// by https://stackoverflow.com/users/445517
/// </summary>
[Benchmark]
public string ByteManipulationDecimal()
{
char[] c = new char[_bytes.Length * 2];
int b;
for (int i = 0; i < _bytes.Length; i++)
{
b = _bytes[i] >> 4;
c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
b = _bytes[i] & 0xF;
c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
}
return new string(c);
}
/// <summary>
/// https://stackoverflow.com/a/22158486/3161322
/// by https://stackoverflow.com/users/278889/patrick
/// </summary>
[Benchmark]
public string WhileLocalLookup()
{
char[] lookup = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
int i = -1, p = 0, l = _bytes.Length;
char[] c = new char[l * 2];
byte d;
--l;
--p;
while (i < l)
{
d = _bytes[++i];
c[++p] = lookup[d >> 4];
c[++p] = lookup[d & 0xF];
}
return new string(c, 0, c.Length);
}
/// <summary>
/// based on WhileLocalLookup
/// by https://github.com/antoninkriz
/// </summary>
[Benchmark]
public string WhilePropertyLookup()
{
int i = -1, p = 0, l = _bytes.Length;
char[] c = new char[l * 2];
byte d;
--l;
--p;
while (i < l)
{
d = _bytes[++i];
c[++p] = HexAlphabetArray[d >> 4];
c[++p] = HexAlphabetArray[d & 0xF];
}
return new string(c, 0, c.Length);
}
/// <summary>
/// https://docs.microsoft.com/en-us/archive/blogs/blambert/blambertcodesnip-fast-byte-array-to-hex-string-conversion
/// by Brian Lambert
/// </summary>
[Benchmark]
public string LookupPerNibble()
{
StringBuilder result = new StringBuilder(_bytes.Length * 2);
foreach (byte b in _bytes)
{
result.Append(HexStringTable[b]);
}
return result.ToString();
}
/// <summary>
/// https://stackoverflow.com/a/5919521/3161322
/// by https://stackoverflow.com/users/610692/nathan-moinvaziri
/// </summary>
[Benchmark]
public string LookupAndShift()
{
StringBuilder result = new StringBuilder(_bytes.Length * 2);
foreach (byte b in _bytes)
{
result.Append(HexAlphabetString[b >> 4]);
result.Append(HexAlphabetString[b & 0xF]);
}
return result.ToString();
}
/// <summary>
/// -
/// by https://github.com/antoninkriz
/// </summary>
[Benchmark]
public string LookupAndShiftAlphabetArray()
{
var res = _bytes.Length * 2 <= 1024 ? stackalloc char[_bytes.Length * 2] : new char[_bytes.Length * 2];
for (int i = 0, j = 0; i < _bytes.Length; ++i, ++j)
{
res[j] = HexAlphabetArray[_bytes[i] >> 4];
res[++j] = HexAlphabetArray[_bytes[i] & 0xF];
}
return new string(res);
}
/// <summary>
/// -
/// by https://github.com/antoninkriz
/// </summary>
[Benchmark]
public string LookupAndShiftAlphabetSpan()
{
var res = _bytes.Length * 2 <= 1024 ? stackalloc char[_bytes.Length * 2] : new char[_bytes.Length * 2];
for (int i = 0, j = 0; i < _bytes.Length; ++i, ++j)
{
res[j] = (char)HexAlphabetSpan[_bytes[i] >> 4];
res[++j] = (char)HexAlphabetSpan[_bytes[i] & 0xF];
}
return new string(res);
}
/// <summary>
/// -
/// by https://github.com/antoninkriz
/// </summary>
[Benchmark]
public string LookupAndShiftAlphabetSpanMultiply()
{
var res = _bytes.Length * 2 <= 1024 ? stackalloc char[_bytes.Length * 2] : new char[_bytes.Length * 2];
for (var i = 0; i < _bytes.Length; ++i)
{
var j = i * 2;
res[j] = (char)HexAlphabetSpan[_bytes[i] >> 4];
res[j + 1] = (char)HexAlphabetSpan[_bytes[i] & 0xF];
}
return new string(res);
}
/// <summary>
/// http://stackoverflow.com/a/24343727/48700
/// by https://stackoverflow.com/users/445517
/// </summary>
[Benchmark]
public string LookupPerByte()
{
var result = new char[_bytes.Length * 2];
for (int i = 0; i < _bytes.Length; i++)
{
var val = Lookup32[_bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
/// <summary>
/// based on LookupPerByte
/// by https://github.com/antoninkriz
/// </summary>
[Benchmark]
public string LookupPerByteSpan()
{
var result = _bytes.Length * 2 <= 1024 ? stackalloc char[_bytes.Length * 2] : new char[_bytes.Length * 2];
for (var i = 0; i < _bytes.Length; i++)
{
var val = Lookup32[_bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
/// <summary>
/// based on LookupPerByte
/// by https://github.com/antoninkriz
/// </summary>
[Benchmark]
public string LookupSpanPerByteSpan()
{
unchecked {
var result = _bytes.Length * 2 <= 1024 ? stackalloc char[_bytes.Length * 2] : new char[_bytes.Length * 2];
for (var i = 0; i < _bytes.Length; i++)
{
var val = Unsafe.ReadUnaligned<uint>(ref MemoryMarshal.GetReference(Lookup32Span.Slice(_bytes[i] * 4, 4)));
result[2 * i] = (char) val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
}
/// <summary>
/// https://stackoverflow.com/a/24343727/3161322
/// by https://stackoverflow.com/users/445517
/// </summary>
[Benchmark]
public unsafe string Lookup32UnsafeDirect()
{
var lookupP = Lookup32UnsafeP;
var result = new string((char)0, _bytes.Length * 2);
fixed (byte* bytesP = _bytes)
fixed (char* resultP = result)
{
uint* resultP2 = (uint*)resultP;
for (int i = 0; i < _bytes.Length; i++)
{
resultP2[i] = lookupP[bytesP[i]];
}
}
return result;
}
/// <summary>
/// https://stackoverflow.com/a/24343727/3161322
/// by https://stackoverflow.com/users/445517
/// </summary>
[Benchmark]
public unsafe string Lookup32SpanUnsafeDirect()
{
var result = new string((char)0, _bytes.Length * 2);
fixed (byte* lookupP = Lookup32Span)
fixed (byte* bytesP = _bytes)
fixed (char* resultP = result)
{
uint* resultP2 = (uint*)resultP;
uint* lookupP2 = (uint*)lookupP;
for (int i = 0; i < _bytes.Length; i++)
{
resultP2[i] = lookupP2[bytesP[i]];
}
}
return result;
}
/// <summary>
/// https://stackoverflow.com/a/68596484/3161322
/// by https://stackoverflow.com/posts/68596484/revisions
/// </summary>
[Benchmark]
public string ConvertToHexString()
=> Convert.ToHexString(_bytes);
}
internal static class Program
{
public static void Main()
{
var summary = BenchmarkRunner.Run<Bench>();
Console.WriteLine(summary);
}
}

Converting byte[] to a hexadecimal string - performance analysis

Updated on: 2022-04-17

Since .NET 5 you should use Convert.ToHexString(bytes[])!

using System;
string result = Convert.ToHexString(bytesToConvert);

About this leaderboard and the benchmark

The comparison from Thymine seems to be outdated and incomplete, especially after .NET 5 with its Convert.ToHexString, so I decided to fall into the bytes to hex string rabbit hole create a new, updated comparison with more methods from answers to both of these two questions.

I went with BenchamrkDotNet instead of a custom-made benchmarking script, which will, hopefully, make the result more accurate.
Always please remember that any micro-benchmarking won't ever represent the real situation and you should do your own tests.

I ran these benchmarks on a Linux with Kernel 5.15.32 on an AMD Ryzen 5800H with 2x8 GB DDR4 @ 2133 MHz.
Be aware, that the whole benchmark might take a lot of time to complete - around 40 minutes on my machine.

UPPERCASE (capitalized) vs lowercase output

All methods mentioned (unless stated otherwise) focus on UPPERCASE output only. That means the output will look like B33F69, not b33f69.

The output from Convert.ToHexString is always uppercase, but thankfully there isn't any significant performance drop when paired with ToLower(), although both unsafe methods will be faster if that's your concern.

Making the string lowercase efficiently might be a challenge in some methods (especially the ones with bit operators magic), but in most, it's enough to just change a parameter X2 to x2 or change the letters from uppercase to lowercase in a mapping.

Leaderboard

Sorted by Mean N=100. The reference point is the StringBuilderForEachByte method.

Method (means are in nanoseconds) Mean N=10 Ratio N=10 Mean N=100 Ratio N=100 Mean N=500 Ratio N=500 Mean N=1k Ratio N=1k Mean N=10k Ratio N=10k Mean N=100k Ratio N=100k
StringBuilderAggregateBytesAppendFormat 364.92 1.48 3,680.00 1.74 18,928.33 1.86 38,362.94 1.87 380,994.74 1.72 42,618,861.57 1.62
StringBuilderForEachAppendFormat 309.59 1.26 3,203.11 1.52 20,775.07 2.04 41,398.07 2.02 426,839.96 1.93 37,220,750.15 1.41
StringJoinSelect 310.84 1.26 2,765.91 1.31 13,549.12 1.33 28,691.16 1.40 304,163.97 1.38 63,541,601.12 2.41
StringConcatSelect 301.34 1.22 2,733.64 1.29 14,449.53 1.42 29,174.83 1.42 307,196.94 1.39 32,877,994.95 1.25
StringJoinArrayConvertAll 279.21 1.13 2,608.71 1.23 13,305.96 1.30 27,207.12 1.32 295,589.61 1.34 62,950,871.38 2.39
StringBuilderAggregateBytesAppend 276.18 1.12 2,599.62 1.23 12,788.11 1.25 26,043.54 1.27 255,389.06 1.16 27,664,344.41 1.05
StringConcatArrayConvertAll 244.81 0.99 2,361.08 1.12 11,881.18 1.16 23,709.21 1.15 265,197.33 1.20 56,044,744.44 2.12
StringBuilderForEachByte 246.09 1.00 2,112.77 1.00 10,200.36 1.00 20,540.77 1.00 220,993.95 1.00 26,387,941.13 1.00
StringBuilderForEachBytePreAllocated 213.85 0.87 1,897.19 0.90 9,340.66 0.92 19,142.27 0.93 204,968.88 0.93 24,902,075.81 0.94
BitConverterReplace 140.09 0.57 1,207.74 0.57 6,170.46 0.60 12,438.23 0.61 145,022.35 0.66 17,719,082.72 0.67
LookupPerNibble 63.78 0.26 421.75 0.20 1,978.22 0.19 3,957.58 0.19 35,358.21 0.16 4,993,649.91 0.19
LookupAndShift 53.22 0.22 311.56 0.15 1,461.15 0.14 2,924.11 0.14 26,180.11 0.12 3,771,827.62 0.14
WhilePropertyLookup 41.83 0.17 308.59 0.15 1,473.10 0.14 2,925.66 0.14 28,440.28 0.13 5,060,341.10 0.19
LookupAndShiftAlphabetArray 37.06 0.15 290.96 0.14 1,387.01 0.14 3,087.86 0.15 29,883.54 0.14 5,136,607.61 0.19
ByteManipulationDecimal 35.29 0.14 251.69 0.12 1,180.38 0.12 2,347.56 0.11 22,731.55 0.10 4,645,593.05 0.18
ByteManipulationHexMultiply 35.45 0.14 235.22 0.11 1,342.50 0.13 2,661.25 0.13 25,810.54 0.12 7,833,116.68 0.30
ByteManipulationHexIncrement 36.43 0.15 234.31 0.11 1,345.38 0.13 2,737.89 0.13 26,413.92 0.12 7,820,224.57 0.30
WhileLocalLookup 42.03 0.17 223.59 0.11 1,016.93 0.10 1,979.24 0.10 19,360.07 0.09 4,150,234.71 0.16
LookupAndShiftAlphabetSpan 30.00 0.12 216.51 0.10 1,020.65 0.10 2,316.99 0.11 22,357.13 0.10 4,580,277.95 0.17
LookupAndShiftAlphabetSpanMultiply 29.04 0.12 207.38 0.10 985.94 0.10 2,259.29 0.11 22,287.12 0.10 4,563,518.13 0.17
LookupPerByte 32.45 0.13 205.84 0.10 951.30 0.09 1,906.27 0.09 18,311.03 0.08 3,908,692.66 0.15
LookupSpanPerByteSpan 25.69 0.10 184.29 0.09 863.79 0.08 2,035.55 0.10 19,448.30 0.09 4,086,961.29 0.15
LookupPerByteSpan 27.03 0.11 184.26 0.09 866.03 0.08 2,005.34 0.10 19,760.55 0.09 4,192,457.14 0.16
Lookup32SpanUnsafeDirect 16.90 0.07 99.20 0.05 436.66 0.04 895.23 0.04 8,266.69 0.04 1,506,058.05 0.06
Lookup32UnsafeDirect 16.51 0.07 98.64 0.05 436.49 0.04 878.28 0.04 8,278.18 0.04 1,753,655.67 0.07
ConvertToHexString 19.27 0.08 64.83 0.03 295.15 0.03 585.86 0.03 5,445.73 0.02 1,478,363.32 0.06
ConvertToHexString.ToLower() 45.66 - 175.16 - 787.86 - 1,516.65 - 13,939.71 - 2,620,046.76 -

Conclusion

The method ConvertToHexString is without any doubt the fastest out there and in my perspective, it should be always used if you have the option - it's very fast and very clean.

using System;

string result = Convert.ToHexString(bytesToConvert);

If not, I decided to highlight two other methods I consider worthy below. I decided not to highlight unsafe methods since such code might be not only, well, unsafe, but most projects I've worked with don't allow such code at all.

Worthy mentions

The first one is LookupPerByteSpan.
The code is almost identical to the code in LookupPerByte by CodesInChaos from this answer. This one is the fastest not-unsafe method benchmarked. The difference between the original and this one is in using stack allocation for shorter inputs (up to 512 bytes). This makes this method around 10 % faster on these inputs, but around 5 % slower on larger ones. Since most of the data I work with is shorter than larger I opted in for this one. LookupSpanPerByteSpan is also very fast, but the size of the code of its ReadOnlySpan<byte> mapping is too large compared to all other methods.

private static readonly uint[] Lookup32 = Enumerable.Range(0, 256).Select(i =>
{
    string s = i.ToString("X2");
    return s[0] + ((uint)s[1] << 16);
}).ToArray();

public string ToHexString(byte[] bytes)
{
    var result = bytes.Length * 2 <= 1024
        ? stackalloc char[bytes.Length * 2]
        : new char[bytes.Length * 2];

    for (int i = 0; i < bytes.Length; i++)
    {
        var val = Lookup32[bytes[i]];
        result[2 * i] = (char)val;
        result[2 * i + 1] = (char)(val >> 16);
    }

    return new string(result);
}

The second one is LookupAndShiftAlphabetSpanMultiply. First I would like to mention that this one is my own creation. I believe that this method is not only pretty fast but also simple to understand. The speed comes from a change that happened in C# 7.3, where declared ReadOnlySpan<byte> methods returning a constant array initialization - new byte {1, 2, 3, ...} - are compiled as the program's static data, therefore omitting a redundant memcpy. [source]

private static ReadOnlySpan<byte> HexAlphabetSpan => new[]
{
    (byte)'0', (byte)'1', (byte)'2', (byte)'3',
    (byte)'4', (byte)'5', (byte)'6', (byte)'7',
    (byte)'8', (byte)'9', (byte)'A', (byte)'B',
    (byte)'C', (byte)'D', (byte)'E', (byte)'F'
};

public static string ToHexString(byte[] bytes)
{
    var res = bytes.Length * 2 <= 1024 ? stackalloc char[bytes.Length * 2] : new char[bytes.Length * 2];

    for (var i = 0; i < bytes.Length; ++i)
    {
        var j = i * 2;
        res[j] = (char)HexAlphabetSpan[bytes[i] >> 4];
        res[j + 1] = (char)HexAlphabetSpan[bytes[i] & 0xF];
    }

    return new string(res);
}
@JaxkDev
Copy link

JaxkDev commented Sep 8, 2023

Interestingly my results differ in ConvertToHex not having the edge anymore.


BenchmarkDotNet v0.13.8, macOS Ventura 13.4.1 (c) (22F770820d) [Darwin 22.5.0]
Apple M1, 1 CPU, 8 logical and 8 physical cores
.NET SDK 7.0.307
  [Host]     : .NET 6.0.21 (6.0.2123.36311), Arm64 RyuJIT AdvSIMD [AttachedDebugger]
  DefaultJob : .NET 6.0.21 (6.0.2123.36311), Arm64 RyuJIT AdvSIMD


Method N Mean Error StdDev Ratio RatioSD
StringBuilderForEachByte 10 237.84 ns 1.067 ns 0.998 ns 1.00 0.00
StringBuilderForEachBytePreAllocated 10 203.76 ns 0.210 ns 0.164 ns 0.86 0.00
StringBuilderForEachAppendFormat 10 342.62 ns 3.039 ns 2.538 ns 1.44 0.01
BitConverterReplace 10 143.96 ns 0.462 ns 0.385 ns 0.61 0.00
StringJoinArrayConvertAll 10 429.06 ns 1.131 ns 1.058 ns 1.80 0.01
StringJoinSelect 10 467.50 ns 1.350 ns 1.262 ns 1.97 0.01
StringConcatArrayConvertAll 10 395.77 ns 1.216 ns 1.137 ns 1.66 0.01
StringConcatSelect 10 271.42 ns 1.118 ns 1.046 ns 1.14 0.00
StringBuilderAggregateBytesAppend 10 264.45 ns 5.305 ns 4.962 ns 1.11 0.02
StringBuilderAggregateBytesAppendFormat 10 343.30 ns 0.485 ns 0.430 ns 1.44 0.00
ByteManipulationHexMultiply 10 29.77 ns 0.060 ns 0.050 ns 0.13 0.00
ByteManipulationHexIncrement 10 33.98 ns 0.138 ns 0.129 ns 0.14 0.00
ByteManipulationDecimal 10 30.81 ns 0.124 ns 0.116 ns 0.13 0.00
WhileLocalLookup 10 34.45 ns 0.129 ns 0.120 ns 0.14 0.00
WhilePropertyLookup 10 57.30 ns 0.384 ns 0.340 ns 0.24 0.00
LookupPerNibble 10 84.54 ns 0.368 ns 0.326 ns 0.36 0.00
LookupAndShift 10 59.18 ns 0.371 ns 0.347 ns 0.25 0.00
LookupAndShiftAlphabetArray 10 35.30 ns 0.048 ns 0.037 ns 0.15 0.00
LookupAndShiftAlphabetSpan 10 25.65 ns 0.050 ns 0.047 ns 0.11 0.00
LookupAndShiftAlphabetSpanMultiply 10 25.65 ns 0.058 ns 0.054 ns 0.11 0.00
LookupPerByte 10 48.39 ns 0.152 ns 0.143 ns 0.20 0.00
LookupPerByteSpan 10 24.98 ns 0.064 ns 0.057 ns 0.11 0.00
LookupSpanPerByteSpan 10 24.36 ns 0.040 ns 0.035 ns 0.10 0.00
Lookup32UnsafeDirect 10 13.05 ns 0.043 ns 0.038 ns 0.05 0.00
Lookup32SpanUnsafeDirect 10 14.02 ns 0.024 ns 0.021 ns 0.06 0.00
ConvertToHexString 10 23.75 ns 0.059 ns 0.049 ns 0.10 0.00
StringBuilderForEachByte 100 2,085.88 ns 1.387 ns 1.230 ns 1.00 0.00
StringBuilderForEachBytePreAllocated 100 1,912.22 ns 1.648 ns 1.541 ns 0.92 0.00
StringBuilderForEachAppendFormat 100 3,056.66 ns 3.854 ns 3.605 ns 1.47 0.00
BitConverterReplace 100 1,318.24 ns 0.904 ns 0.801 ns 0.63 0.00
StringJoinArrayConvertAll 100 4,285.60 ns 6.087 ns 5.694 ns 2.05 0.00
StringJoinSelect 100 4,323.61 ns 3.820 ns 3.386 ns 2.07 0.00
StringConcatArrayConvertAll 100 3,918.45 ns 3.504 ns 2.926 ns 1.88 0.00
StringConcatSelect 100 2,470.54 ns 4.102 ns 3.837 ns 1.18 0.00
StringBuilderAggregateBytesAppend 100 2,293.31 ns 9.339 ns 8.278 ns 1.10 0.00
StringBuilderAggregateBytesAppendFormat 100 3,299.83 ns 4.481 ns 3.972 ns 1.58 0.00
ByteManipulationHexMultiply 100 231.27 ns 1.840 ns 1.721 ns 0.11 0.00
ByteManipulationHexIncrement 100 250.76 ns 1.449 ns 1.355 ns 0.12 0.00
ByteManipulationDecimal 100 239.13 ns 1.598 ns 1.495 ns 0.11 0.00
WhileLocalLookup 100 226.82 ns 0.768 ns 0.681 ns 0.11 0.00
WhilePropertyLookup 100 327.87 ns 1.544 ns 1.445 ns 0.16 0.00
LookupPerNibble 100 437.87 ns 1.275 ns 1.193 ns 0.21 0.00
LookupAndShift 100 287.10 ns 0.448 ns 0.397 ns 0.14 0.00
LookupAndShiftAlphabetArray 100 303.72 ns 0.388 ns 0.363 ns 0.15 0.00
LookupAndShiftAlphabetSpan 100 203.36 ns 0.279 ns 0.233 ns 0.10 0.00
LookupAndShiftAlphabetSpanMultiply 100 203.13 ns 0.287 ns 0.269 ns 0.10 0.00
LookupPerByte 100 231.47 ns 1.524 ns 1.425 ns 0.11 0.00
LookupPerByteSpan 100 191.86 ns 0.239 ns 0.212 ns 0.09 0.00
LookupSpanPerByteSpan 100 191.09 ns 0.266 ns 0.249 ns 0.09 0.00
Lookup32UnsafeDirect 100 77.85 ns 0.101 ns 0.084 ns 0.04 0.00
Lookup32SpanUnsafeDirect 100 88.02 ns 0.479 ns 0.448 ns 0.04 0.00
ConvertToHexString 100 187.51 ns 0.370 ns 0.328 ns 0.09 0.00
StringBuilderForEachByte 500 10,528.61 ns 210.480 ns 206.720 ns 1.00 0.00
StringBuilderForEachBytePreAllocated 500 10,534.04 ns 210.322 ns 420.037 ns 1.01 0.05
StringBuilderForEachAppendFormat 500 15,479.46 ns 56.450 ns 52.803 ns 1.47 0.03
BitConverterReplace 500 6,915.49 ns 133.916 ns 164.461 ns 0.66 0.02
StringJoinArrayConvertAll 500 21,708.91 ns 54.372 ns 48.199 ns 2.06 0.04
StringJoinSelect 500 21,395.79 ns 55.790 ns 49.457 ns 2.03 0.04
StringConcatArrayConvertAll 500 20,041.48 ns 33.697 ns 29.871 ns 1.90 0.04
StringConcatSelect 500 12,143.63 ns 38.640 ns 36.144 ns 1.15 0.02
StringBuilderAggregateBytesAppend 500 11,538.70 ns 27.429 ns 25.658 ns 1.10 0.02
StringBuilderAggregateBytesAppendFormat 500 16,055.28 ns 19.509 ns 17.294 ns 1.52 0.03
ByteManipulationHexMultiply 500 1,149.94 ns 7.179 ns 6.715 ns 0.11 0.00
ByteManipulationHexIncrement 500 1,245.40 ns 1.246 ns 1.104 ns 0.12 0.00
ByteManipulationDecimal 500 1,138.29 ns 1.392 ns 1.234 ns 0.11 0.00
WhileLocalLookup 500 1,066.51 ns 1.189 ns 0.928 ns 0.10 0.00
WhilePropertyLookup 500 1,512.84 ns 1.883 ns 1.572 ns 0.14 0.00
LookupPerNibble 500 1,976.32 ns 1.228 ns 1.089 ns 0.19 0.00
LookupAndShift 500 1,291.65 ns 1.080 ns 0.958 ns 0.12 0.00
LookupAndShiftAlphabetArray 500 1,491.65 ns 1.076 ns 0.954 ns 0.14 0.00
LookupAndShiftAlphabetSpan 500 1,016.49 ns 0.594 ns 0.496 ns 0.10 0.00
LookupAndShiftAlphabetSpanMultiply 500 1,017.71 ns 0.700 ns 0.584 ns 0.10 0.00
LookupPerByte 500 1,044.89 ns 0.873 ns 0.681 ns 0.10 0.00
LookupPerByteSpan 500 957.50 ns 0.824 ns 0.731 ns 0.09 0.00
LookupSpanPerByteSpan 500 955.68 ns 0.391 ns 0.305 ns 0.09 0.00
Lookup32UnsafeDirect 500 370.71 ns 1.284 ns 1.138 ns 0.04 0.00
Lookup32SpanUnsafeDirect 500 427.42 ns 1.390 ns 1.232 ns 0.04 0.00
ConvertToHexString 500 898.48 ns 2.035 ns 1.904 ns 0.09 0.00
StringBuilderForEachByte 1000 20,056.53 ns 36.171 ns 33.834 ns 1.00 0.00
StringBuilderForEachBytePreAllocated 1000 19,643.61 ns 35.202 ns 32.928 ns 0.98 0.00
StringBuilderForEachAppendFormat 1000 29,955.72 ns 66.028 ns 61.763 ns 1.49 0.00
BitConverterReplace 1000 13,521.26 ns 12.064 ns 10.694 ns 0.67 0.00
StringJoinArrayConvertAll 1000 43,393.05 ns 208.626 ns 195.149 ns 2.16 0.01
StringJoinSelect 1000 43,191.71 ns 74.393 ns 62.121 ns 2.15 0.01
StringConcatArrayConvertAll 1000 40,258.95 ns 70.732 ns 66.162 ns 2.01 0.01
StringConcatSelect 1000 24,475.77 ns 51.424 ns 48.102 ns 1.22 0.00
StringBuilderAggregateBytesAppend 1000 23,325.98 ns 95.571 ns 84.721 ns 1.16 0.00
StringBuilderAggregateBytesAppendFormat 1000 32,455.55 ns 82.194 ns 76.884 ns 1.62 0.00
ByteManipulationHexMultiply 1000 2,327.09 ns 2.206 ns 1.842 ns 0.12 0.00
ByteManipulationHexIncrement 1000 2,510.61 ns 3.659 ns 3.244 ns 0.13 0.00
ByteManipulationDecimal 1000 2,303.40 ns 8.798 ns 8.230 ns 0.11 0.00
WhileLocalLookup 1000 2,153.22 ns 7.137 ns 6.326 ns 0.11 0.00
WhilePropertyLookup 1000 3,050.74 ns 18.448 ns 15.405 ns 0.15 0.00
LookupPerNibble 1000 3,969.18 ns 11.210 ns 10.486 ns 0.20 0.00
LookupAndShift 1000 2,626.01 ns 16.584 ns 12.948 ns 0.13 0.00
LookupAndShiftAlphabetArray 1000 3,095.14 ns 12.345 ns 10.944 ns 0.15 0.00
LookupAndShiftAlphabetSpan 1000 2,137.07 ns 6.133 ns 5.436 ns 0.11 0.00
LookupAndShiftAlphabetSpanMultiply 1000 2,137.69 ns 5.689 ns 5.322 ns 0.11 0.00
LookupPerByte 1000 2,103.22 ns 1.810 ns 1.605 ns 0.10 0.00
LookupPerByteSpan 1000 2,018.43 ns 3.297 ns 2.753 ns 0.10 0.00
LookupSpanPerByteSpan 1000 2,009.70 ns 2.602 ns 2.306 ns 0.10 0.00
Lookup32UnsafeDirect 1000 725.93 ns 1.321 ns 1.171 ns 0.04 0.00
Lookup32SpanUnsafeDirect 1000 840.66 ns 1.907 ns 1.691 ns 0.04 0.00
ConvertToHexString 1000 1,796.54 ns 3.639 ns 3.039 ns 0.09 0.00
StringBuilderForEachByte 10000 196,282.19 ns 305.085 ns 270.450 ns 1.00 0.00
StringBuilderForEachBytePreAllocated 10000 194,644.96 ns 452.617 ns 423.378 ns 0.99 0.00
StringBuilderForEachAppendFormat 10000 297,113.65 ns 533.016 ns 472.504 ns 1.51 0.00
BitConverterReplace 10000 138,321.52 ns 1,711.955 ns 1,517.604 ns 0.70 0.01
StringJoinArrayConvertAll 10000 435,626.51 ns 723.156 ns 641.059 ns 2.22 0.00
StringJoinSelect 10000 440,929.32 ns 533.375 ns 472.823 ns 2.25 0.00
StringConcatArrayConvertAll 10000 401,029.37 ns 862.413 ns 764.506 ns 2.04 0.00
StringConcatSelect 10000 243,800.72 ns 834.358 ns 780.459 ns 1.24 0.00
StringBuilderAggregateBytesAppend 10000 227,814.99 ns 318.808 ns 298.213 ns 1.16 0.00
StringBuilderAggregateBytesAppendFormat 10000 320,601.96 ns 1,474.302 ns 1,231.109 ns 1.63 0.01
ByteManipulationHexMultiply 10000 33,101.51 ns 689.607 ns 2,033.321 ns 0.17 0.01
ByteManipulationHexIncrement 10000 33,625.56 ns 760.892 ns 2,243.509 ns 0.17 0.01
ByteManipulationDecimal 10000 22,011.74 ns 19.860 ns 17.605 ns 0.11 0.00
WhileLocalLookup 10000 20,467.12 ns 17.853 ns 15.826 ns 0.10 0.00
WhilePropertyLookup 10000 29,269.18 ns 53.099 ns 44.340 ns 0.15 0.00
LookupPerNibble 10000 37,557.44 ns 41.125 ns 38.468 ns 0.19 0.00
LookupAndShift 10000 24,100.94 ns 37.837 ns 31.595 ns 0.12 0.00
LookupAndShiftAlphabetArray 10000 29,731.14 ns 76.045 ns 71.133 ns 0.15 0.00
LookupAndShiftAlphabetSpan 10000 20,403.18 ns 13.359 ns 10.430 ns 0.10 0.00
LookupAndShiftAlphabetSpanMultiply 10000 20,404.08 ns 17.040 ns 15.939 ns 0.10 0.00
LookupPerByte 10000 20,043.81 ns 31.858 ns 29.800 ns 0.10 0.00
LookupPerByteSpan 10000 19,253.09 ns 11.706 ns 10.377 ns 0.10 0.00
LookupSpanPerByteSpan 10000 19,191.82 ns 31.709 ns 28.109 ns 0.10 0.00
Lookup32UnsafeDirect 10000 6,598.09 ns 12.259 ns 11.467 ns 0.03 0.00
Lookup32SpanUnsafeDirect 10000 7,776.98 ns 15.310 ns 14.321 ns 0.04 0.00
ConvertToHexString 10000 17,396.98 ns 37.913 ns 35.464 ns 0.09 0.00
StringBuilderForEachByte 1000000 20,065,159.12 ns 82,380.461 ns 77,058.734 ns 1.00 0.00
StringBuilderForEachBytePreAllocated 1000000 19,948,366.75 ns 55,814.278 ns 52,208.710 ns 0.99 0.00
StringBuilderForEachAppendFormat 1000000 30,150,187.98 ns 54,543.488 ns 48,351.382 ns 1.50 0.01
BitConverterReplace 1000000 17,787,739.78 ns 11,487.167 ns 9,592.304 ns 0.89 0.00
StringJoinArrayConvertAll 1000000 78,098,084.11 ns 1,088,929.245 ns 1,018,585.092 ns 3.89 0.06
StringJoinSelect 1000000 78,542,908.72 ns 983,898.696 ns 920,339.451 ns 3.91 0.05
StringConcatArrayConvertAll 1000000 75,618,245.62 ns 1,353,141.617 ns 1,265,729.509 ns 3.77 0.07
StringConcatSelect 1000000 25,794,411.19 ns 159,624.812 ns 149,313.149 ns 1.29 0.01
StringBuilderAggregateBytesAppend 1000000 23,108,939.11 ns 61,818.410 ns 57,824.979 ns 1.15 0.01
StringBuilderAggregateBytesAppendFormat 1000000 32,626,777.45 ns 64,746.866 ns 54,066.565 ns 1.63 0.01
ByteManipulationHexMultiply 1000000 7,398,623.70 ns 12,186.948 ns 10,803.412 ns 0.37 0.00
ByteManipulationHexIncrement 1000000 7,412,801.69 ns 11,932.169 ns 11,161.358 ns 0.37 0.00
ByteManipulationDecimal 1000000 2,709,942.94 ns 15,755.294 ns 14,737.511 ns 0.14 0.00
WhileLocalLookup 1000000 2,542,603.15 ns 5,250.148 ns 4,910.992 ns 0.13 0.00
WhilePropertyLookup 1000000 3,411,707.51 ns 12,319.409 ns 11,523.583 ns 0.17 0.00
LookupPerNibble 1000000 4,259,486.20 ns 8,040.466 ns 7,521.057 ns 0.21 0.00
LookupAndShift 1000000 2,929,316.41 ns 6,560.232 ns 5,478.091 ns 0.15 0.00
LookupAndShiftAlphabetArray 1000000 3,467,651.91 ns 8,179.410 ns 7,250.834 ns 0.17 0.00
LookupAndShiftAlphabetSpan 1000000 2,503,873.00 ns 8,158.221 ns 7,631.205 ns 0.12 0.00
LookupAndShiftAlphabetSpanMultiply 1000000 2,531,838.27 ns 19,972.338 ns 18,682.138 ns 0.13 0.00
LookupPerByte 1000000 2,490,824.21 ns 7,110.139 ns 6,650.828 ns 0.12 0.00
LookupPerByteSpan 1000000 2,417,645.18 ns 4,863.089 ns 4,060.899 ns 0.12 0.00
LookupSpanPerByteSpan 1000000 2,403,569.69 ns 6,160.393 ns 5,762.436 ns 0.12 0.00
Lookup32UnsafeDirect 1000000 735,553.43 ns 1,565.430 ns 1,464.304 ns 0.04 0.00
Lookup32SpanUnsafeDirect 1000000 851,711.58 ns 3,990.730 ns 3,332.440 ns 0.04 0.00
ConvertToHexString 1000000 1,800,532.67 ns 2,647.569 ns 2,476.537 ns 0.09 0.00

@damieng
Copy link

damieng commented Feb 11, 2024

I found that a slight modification to LookupAndShift to use an array instead of a string builder is giving even better results thanLookupPerByteSpan here. (Still not as good as the Unsafe or the .NET 5 Convert.ToHex but could be good if you can't use either).

[Benchmark]
private const string HexAlphabetString = "0123456789ABCDEF";

public string LookupAndShiftArray()
{
    var result = _bytes.Length * 2 <= 1024 ? stackalloc char[_bytes.Length * 2] : new char[_bytes.Length * 2];
    int i = 0;
    foreach (byte b in _bytes)
    {
        result[i++] = HexAlphabetString[b >> 4];
        result[i++] = HexAlphabetString[b & 0xF];
    }

    return new string(result);
}

Benchmarks for 8-256:

| Method              | N   | Mean      | Error    | StdDev   |
|-------------------- |---- |----------:|---------:|---------:|
| LookupAndShiftArray | 8   |  14.33 ns | 0.320 ns | 0.328 ns |
| LookupPerByteSpan   | 8   |  16.12 ns | 0.356 ns | 0.554 ns |
| LookupAndShiftArray | 16  |  21.67 ns | 0.217 ns | 0.192 ns |
| LookupPerByteSpan   | 16  |  24.25 ns | 0.270 ns | 0.253 ns |
| LookupAndShiftArray | 32  |  36.65 ns | 0.533 ns | 0.498 ns |
| LookupPerByteSpan   | 32  |  49.45 ns | 0.946 ns | 0.885 ns |
| LookupAndShiftArray | 64  |  67.71 ns | 1.259 ns | 1.593 ns |
| LookupPerByteSpan   | 64  |  84.56 ns | 0.993 ns | 0.929 ns |
| LookupAndShiftArray | 128 | 134.92 ns | 1.390 ns | 1.300 ns |
| LookupPerByteSpan   | 128 | 157.19 ns | 1.938 ns | 1.812 ns |
| LookupAndShiftArray | 256 | 259.92 ns | 2.214 ns | 2.071 ns |
| LookupPerByteSpan   | 256 | 297.41 ns | 4.123 ns | 3.857 ns |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment