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);
}
@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