How can you convert a byte array to a hexadecimal string and vice versa?
-
10The accepted answer below appear to allocate a horrible amount of strings in the string to bytes conversion. I'm wondering how this impacts performance – Wim Coenen Mar 06 '09 at 16:41
53 Answers
You can use Convert.ToHexString
starting with .NET 5.
There's also a method for the reverse operation: Convert.FromHexString
.
For older versions of .NET you can either use:
public static string ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);
foreach (byte b in ba)
hex.AppendFormat("{0:x2}", b);
return hex.ToString();
}
or:
public static string ByteArrayToString(byte[] ba)
{
return BitConverter.ToString(ba).Replace("-","");
}
There are even more variants of doing it, for example here.
The reverse conversion would go like this:
public static byte[] StringToByteArray(String hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
Using Substring
is the best option in combination with Convert.ToByte
. See this answer for more information. If you need better performance, you must avoid Convert.ToByte
before you can drop SubString
.
-
31You're using SubString. Doesn't this loop allocate a horrible amount of string objects? – Wim Coenen Mar 06 '09 at 16:36
-
33Honestly - until it tears down performance dramatically, I would tend to ignore this and trust the Runtime and the GC to take care of it. – Tomalak Mar 06 '09 at 17:11
-
1FWIW I could get a 4x speed-up on my machine by eliminating sub-string. Can't post the code because I wrote this for my employer. – Wim Coenen Mar 08 '09 at 18:26
-
2Your StringToByteArray() fails if you have an odd number of hex characters. This is easily fixed by padding odd strings with a "0" at the front. – Carlos Rendon Nov 23 '09 at 17:21
-
94Because a byte is two nibbles, any hex string that validly represents a byte array must have an even character count. A 0 should not be added anywhere - to add one would be making an assumption about invalid data that is potentially dangerous. If anything, the StringToByteArray method should throw a FormatException if the hex string contains an odd number of characters. – David Boike Mar 09 '10 at 19:01
-
The first example returns a different value then the second one. Can anyone explain why? – iJK Apr 20 '10 at 14:39
-
@DavidBoike While I concur, if you're interested in the mathematical representation only, it is okay to prepend the entire string with zeroes, but you are correct, it is almost certainly bad data, because you're asking for bytes in return, and bytes will always be appropriately padded. – McKay Jun 22 '12 at 19:22
-
2See http://stackoverflow.com/a/14332574/22656 for a version which doesn't use Substring. – Jon Skeet Jan 15 '13 at 07:09
-
@DavidBoike is "F" an invalid hex string? Isn't it the same as "0F"? Thus you could have a hex string with an odd character count. – 00jt Jan 28 '13 at 15:15
-
8@00jt You must make an assumption that F == 0F. Either it is the same as 0F, or the input was clipped and F is actually the start of something you have not received. It is up to your context to make those assumptions, but I believe a general purpose function should reject odd characters as invalid instead of making that assumption for the calling code. – David Boike Jan 28 '13 at 15:35
-
12@DavidBoike The question had NOTHING to do with "how to handle possibly clipped stream values" Its talking about a String. String myValue = 10.ToString("X"); myValue is "A" not "0A". Now go read that string back into bytes, oops you broke it. – 00jt Jan 30 '13 at 19:25
-
1It's stated explicitly in the question that they'd like it to actually convert back. F != 0F, so you'd end up with a different result. Also, it's just generally poor practice to make unnecessary assumptions. Your example is silly, you'd use ToString("X2"). Your example is identical saying if you use ToString("X3") it doesn't go through hex-parsing functions. Of course not because you've encoded it in a non-standard way. – Rushyo Oct 24 '13 at 10:18
-
In the .Net Micro Framework there's no `Convert.ToByte` taking two arguments nor `StringReader`, it'd be great to see a StringToByteArray in the answer without those used. – dumbledad Jul 19 '14 at 20:43
-
-
@Link points to spam. thinksharp.org/hex-string-to-byte-array-converter/ – kanchirk Mar 18 '15 at 17:09
-
@DavidBoike I never knew what a nibble was. Your comment made me aware. Thanks. – RBT Nov 21 '16 at 07:18
-
1`StringToByteArray` is a terrible name for this function. Call it `EncodeHex` or `DecodeHex` or create a class `Hex` and put in `Encode` or `Decode` methods. You could also decode base64, encode as UTF-8 or UTF-16 and call it `StringToByteArray`. Somewhere you need to put in the `Hex` part in the name. – Maarten Bodewes Jun 11 '18 at 00:40
-
-
`Convert.ToHexString` and `Convert.FromHexString` is the way to go in most cases. But (disclaimer) I'm the author of assembly doing same thing a) before .NET 5 b) faster (with SIMD - SSE2/AVX2). If Base16 conversion is critical in your application use: https://github.com/MiloszKrajewski/K4os.Text.BaseX – Milosz Krajewski Jan 08 '23 at 18:35
Performance Analysis
Note: new leader as of 2015-08-20.
I ran each of the various conversion methods through some crude Stopwatch
performance testing, a run with a random sentence (n=61, 1000 iterations) and a run with a Project Gutenburg text (n=1,238,957, 150 iterations). Here are the results, roughly from fastest to slowest. All measurements are in ticks (10,000 ticks = 1 ms) and all relative notes are compared to the [slowest] StringBuilder
implementation. For the code used, see below or the test framework repo where I now maintain the code for running this.
Disclaimer
WARNING: Do not rely on these stats for anything concrete; they are simply a sample run of sample data. If you really need top-notch performance, please test these methods in an environment representative of your production needs with data representative of what you will use.
Results
- Lookup by byte
unsafe
(via CodesInChaos) (added to test repo by airbreather)- Text: 4,727.85 (105.2X)
- Sentence: 0.28 (99.7X)
- Lookup by byte (via CodesInChaos)
- Text: 10,853.96 (45.8X faster)
- Sentence: 0.65 (42.7X faster)
- Byte Manipulation 2 (via CodesInChaos)
- Text: 12,967.69 (38.4X faster)
- Sentence: 0.73 (37.9X faster)
- Byte Manipulation (via Waleed Eissa)
- Text: 16,856.64 (29.5X faster)
- Sentence: 0.70 (39.5X faster)
- Lookup/Shift (via Nathan Moinvaziri)
- Text: 23,201.23 (21.4X faster)
- Sentence: 1.24 (22.3X faster)
- Lookup by nibble (via Brian Lambert)
- Text: 23,879.41 (20.8X faster)
- Sentence: 1.15 (23.9X faster)
BitConverter
(via Tomalak)- Text: 113,269.34 (4.4X faster)
- Sentence: 9.98 (2.8X faster)
{SoapHexBinary}.ToString
(via Mykroft)- Text: 178,601.39 (2.8X faster)
- Sentence: 10.68 (2.6X faster)
{byte}.ToString("X2")
(usingforeach
) (derived from Will Dean's answer)- Text: 308,805.38 (2.4X faster)
- Sentence: 16.89 (2.4X faster)
{byte}.ToString("X2")
(using{IEnumerable}.Aggregate
, requires System.Linq) (via Mark)- Text: 352,828.20 (2.1X faster)
- Sentence: 16.87 (2.4X faster)
Array.ConvertAll
(usingstring.Join
) (via Will Dean)- Text: 675,451.57 (1.1X faster)
- Sentence: 17.95 (2.2X faster)
Array.ConvertAll
(usingstring.Concat
, requires .NET 4.0) (via Will Dean)- Text: 752,078.70 (1.0X faster)
- Sentence: 18.28 (2.2X faster)
{StringBuilder}.AppendFormat
(usingforeach
) (via Tomalak)- Text: 672,115.77 (1.1X faster)
- Sentence: 36.82 (1.1X faster)
{StringBuilder}.AppendFormat
(using{IEnumerable}.Aggregate
, requires System.Linq) (derived from Tomalak's answer)- Text: 718,380.63 (1.0X faster)
- Sentence: 39.71 (1.0X faster)
Lookup tables have taken the lead over byte manipulation. Basically, there is some form of precomputing what any given nibble or byte will be in hex. Then, as you rip through the data, you simply look up the next portion to see what hex string it would be. That value is then added to the resulting string output in some fashion. For a long time byte manipulation, potentially harder to read by some developers, was the top-performing approach.
Your best bet is still going to be finding some representative data and trying it out in a production-like environment. If you have different memory constraints, you may prefer a method with fewer allocations to one that would be faster but consume more memory.
Testing Code
Feel free to play with the testing code I used. A version is included here but feel free to clone the repo and add your own methods. Please submit a pull request if you find anything interesting or want to help improve the testing framework it uses.
- Add the new static method (
Func<byte[], string>
) to /Tests/ConvertByteArrayToHexString/Test.cs. - Add that method's name to the
TestCandidates
return value in that same class. - Make sure you are running the input version you want, sentence or text, by toggling the comments in
GenerateTestInput
in that same class. - Hit F5 and wait for the output (an HTML dump is also generated in the /bin folder).
static string ByteArrayToHexStringViaStringJoinArrayConvertAll(byte[] bytes) {
return string.Join(string.Empty, Array.ConvertAll(bytes, b => b.ToString("X2")));
}
static string ByteArrayToHexStringViaStringConcatArrayConvertAll(byte[] bytes) {
return string.Concat(Array.ConvertAll(bytes, b => b.ToString("X2")));
}
static string ByteArrayToHexStringViaBitConverter(byte[] bytes) {
string hex = BitConverter.ToString(bytes);
return hex.Replace("-", "");
}
static string ByteArrayToHexStringViaStringBuilderAggregateByteToString(byte[] bytes) {
return bytes.Aggregate(new StringBuilder(bytes.Length * 2), (sb, b) => sb.Append(b.ToString("X2"))).ToString();
}
static string ByteArrayToHexStringViaStringBuilderForEachByteToString(byte[] bytes) {
StringBuilder hex = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
hex.Append(b.ToString("X2"));
return hex.ToString();
}
static string ByteArrayToHexStringViaStringBuilderAggregateAppendFormat(byte[] bytes) {
return bytes.Aggregate(new StringBuilder(bytes.Length * 2), (sb, b) => sb.AppendFormat("{0:X2}", b)).ToString();
}
static string ByteArrayToHexStringViaStringBuilderForEachAppendFormat(byte[] bytes) {
StringBuilder hex = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
hex.AppendFormat("{0:X2}", b);
return hex.ToString();
}
static string ByteArrayToHexViaByteManipulation(byte[] bytes) {
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);
}
static string ByteArrayToHexViaByteManipulation2(byte[] bytes) {
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);
}
static string ByteArrayToHexViaSoapHexBinary(byte[] bytes) {
SoapHexBinary soapHexBinary = new SoapHexBinary(bytes);
return soapHexBinary.ToString();
}
static string ByteArrayToHexViaLookupAndShift(byte[] bytes) {
StringBuilder result = new StringBuilder(bytes.Length * 2);
string hexAlphabet = "0123456789ABCDEF";
foreach (byte b in bytes) {
result.Append(hexAlphabet[(int)(b >> 4)]);
result.Append(hexAlphabet[(int)(b & 0xF)]);
}
return result.ToString();
}
static readonly uint* _lookup32UnsafeP = (uint*)GCHandle.Alloc(_Lookup32, GCHandleType.Pinned).AddrOfPinnedObject();
static string ByteArrayToHexViaLookup32UnsafeDirect(byte[] bytes) {
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;
}
static uint[] _Lookup32 = Enumerable.Range(0, 255).Select(i => {
string s = i.ToString("X2");
return ((uint)s[0]) + ((uint)s[1] << 16);
}).ToArray();
static string ByteArrayToHexViaLookupPerByte(byte[] bytes) {
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);
}
static string ByteArrayToHexViaLookup(byte[] bytes) {
string[] hexStringTable = new string[] {
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B", "1C", "1D", "1E", "1F",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C", "3D", "3E", "3F",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4A", "4B", "4C", "4D", "4E", "4F",
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D", "5E", "5F",
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6A", "6B", "6C", "6D", "6E", "6F",
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E", "7F",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8A", "8B", "8C", "8D", "8E", "8F",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F",
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF",
"B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF",
"C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF",
"E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF",
"F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF",
};
StringBuilder result = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes) {
result.Append(hexStringTable[b]);
}
return result.ToString();
}
Update (2010-01-13)
Added Waleed's answer to analysis. Quite fast.
Update (2011-10-05)
Added string.Concat
Array.ConvertAll
variant for completeness (requires .NET 4.0). On par with string.Join
version.
Update (2012-02-05)
Test repo includes more variants such as StringBuilder.Append(b.ToString("X2"))
. None upset the results any. foreach
is faster than {IEnumerable}.Aggregate
, for instance, but BitConverter
still wins.
Update (2012-04-03)
Added Mykroft's SoapHexBinary
answer to analysis, which took over third place.
Update (2013-01-15)
Added CodesInChaos's byte manipulation answer, which took over first place (by a large margin on large blocks of text).
Update (2013-05-23)
Added Nathan Moinvaziri's lookup answer and the variant from Brian Lambert's blog. Both rather fast, but not taking the lead on the test machine I used (AMD Phenom 9750).
Update (2014-07-31)
Added @CodesInChaos's new byte-based lookup answer. It appears to have taken the lead on both the sentence tests and the full-text tests.
Update (2015-08-20)
Added airbreather's optimizations and unsafe
variant to this answer's repo. If you want to play in the unsafe game, you can get some huge performance gains over any of the prior top winners on both short strings and large texts.
-
Would you care to test the code from Waleed's answer? It seems to be very fast. http://stackoverflow.com/questions/311165/how-do-you-convert-byte-array-to-hexadecimal-string-and-vice-versa-in-c/632920#632920 – Cristian Diaconescu Dec 24 '09 at 21:16
-
7Despite making the code available for you to do the very thing you requested on your own, I updated the testing code to include Waleed answer. All grumpiness aside, it is much faster. – patridge Jan 13 '10 at 16:29
-
I got a different result when I used "ByteArrayToHexStringViaBitConverter" and "ByteArrayToHexStringViaStringBuilder". The latter one turned out to be "right". Is there any reason why the result from the two functions should be different? – iJK Apr 20 '10 at 22:11
-
If it is still happening, the best I can guess at this point is some sort of system culture variation that is affecting the results. – patridge Oct 04 '11 at 21:36
-
Can you add my solution? In my test it was about 25% faster than `ByteArrayToHexViaByteManipulation` – CodesInChaos Jan 15 '13 at 08:57
-
2@CodesInChaos Done. And it won in my tests by quite a bit as well. I don't pretend to fully understand either of the top methods yet, but they are easily hidden from direct interaction. – patridge Jan 15 '13 at 18:01
-
Since C# 4 (VS2010), the most natural thing is `String.Concat(bytes.Select(b => b.ToString("X2")))`. Note that we don't allocate and copy into an entire new temporary array, as with `Array.ConvertAll<>`. This solution was also given by @AllonGuralnek in a comment to Will Dean's answer. I think we can agree people would use LINQ `Select` nowadays. – Jeppe Stig Nielsen Apr 08 '13 at 18:03
-
6This answer has no intention of answering the question of what is "natural" or commonplace. The goal is to give people some basic performance benchmarks since, when you need to do these conversion, you tend to do them a lot. If someone needs raw speed, they just run the benchmarks with some appropriate test data in their desired computing environment. Then, tuck that method away into an extension method where you never look its implementation again (e.g., `bytes.ToHexStringAtLudicrousSpeed()`). – patridge Apr 08 '13 at 20:37
-
Give this answer a try: http://stackoverflow.com/a/5919521/356218 In my performance tests (on that answer somewhere) it is slightly faster than `ByteArrayToHexViaByteManipulation` but much cleaner code – Thymine May 06 '13 at 23:09
-
I put it in the [testing framework](https://github.com/patridge/PerformanceStubs) and gave it a run. It's certainly fast enough for people to consider in their own use, though it didn't take the lead in tests. – patridge Jun 04 '13 at 15:16
-
@CodesInChaos I've got a "bit fiddle" implementation using a single loop for *decoding* a hex string. Want to test? Damn for Codes for beating me to a bit fiddle implementation :P – Maarten Bodewes Jan 20 '14 at 23:20
-
2Just produced a high performance lookup table based implementation. Its safe variant is about 30% faster than the current leader on my CPU. The unsafe variants are even faster. http://stackoverflow.com/a/24343727/445517 – CodesInChaos Jun 21 '14 at 17:12
-
@CodesInChaos It's definitely faster in that silly [perf test framework](https://github.com/patridge/PerformanceStubs) I put together for this answer. I updated the answer and the test framework repo. I only added the safe variant, so you probably have some seriously unholy speed with that unsafe version. Nicely done, sir. – patridge Jul 31 '14 at 22:42
-
Just realized, the `unsafe` lookup table in the performance test harness is only correct when BitConverter.IsLittleEndian (https://msdn.microsoft.com/en-us/library/system.bitconverter.islittleendian.aspx) is true. @CodesInChaos original code handles this properly. Not a huge issue, but there you have it. – Joe Amenta Aug 27 '15 at 23:11
-
Thanks, @JoeAmenta. I've [filed a bug on the project repo](https://github.com/patridge/PerformanceStubs/issues/4) to remind me to fix it. Without looking at it too much right now, it looks like it should be simple enough given CodesInChaos' sample. – patridge Aug 28 '15 at 16:04
-
Found a bug in `ByteArrayToHexViaLookupPerByte` causing IndexOutOfRangeException: the _Lookup32 array should have 256 elements rather than 255. E.g. try executing `ByteArrayToHexViaLookupPerByte(new byte[] { 255 })` – lethek Aug 08 '16 at 00:51
-
Great answer. I used the second option in your result list http://stackoverflow.com/a/24343727/48700 and tested it with a SignalR websocket to broadcast pictures to listeners without using any files. On the javascript side: var src = 'data:image/jpeg;base64,' + hexToBase64(binimage); $("#aha").attr("src", src); – Goodies Sep 06 '16 at 12:22
-
3@Goodies I've discovered that the simple Convert.ToBase64String() is VERY fast (faster than Lookup by byte (via CodesInChaos) ) in my testing - so if anyone doesn't care about the output being hexadecimal, that's a quick one-line replacement. – GilesDMiddleton Aug 10 '18 at 09:44
-
@CodesInChaos your byte manipulation function is excellent, thank you! – HamsterWithPitchfork Mar 15 '19 at 11:27
-
2bin to hex can be vectorized with SIMD. For example, https://github.com/darealshinji/vectorclass/blob/28d002ca908af1e7b5956c7bffef2bfed7923c6b/special/decimal.h#L835 Agner Fog's vectorclass library C++ wrappers for Intel intrinsics (SSE/AVX). My answer on [How to convert a binary integer number to a hex string?](https://stackoverflow.com/q/53823756) is in assembly so could be ported to C# intrinsics or C++ intrinsics with equal effort. Producing a vector of 16 or 32B of hex data at a time with a few shuffles and shifts can go at least 5 times faster than a byte lookup table. @CodesInChaos – Peter Cordes Apr 13 '20 at 19:32
-
Or with AVX512VBMI on Ice Lake, 8, 16, 32, or 64 bytes of ASCII hex data (from half that input width) in a couple clock cycles with a couple instructions, notably [`vpmultishiftqb`](http://felixcloutier.com/x86/VPMULTISHIFTQB.html) that does a parallel bitfield extract. Of course then the problem becomes formatting the hex digits with spaces between chunks, or `'-'` for a UUID. For example, I was playing around with SIMD for libuuid (https://marc.info/?l=util-linux-ng&m=158526494427117&w=2) / https://godbolt.org/z/WB-C_y SSSE3 is about 14x faster than optimized scalar C++ on Skylake for UUIDs. – Peter Cordes Apr 13 '20 at 19:41
-
I think the 2014 byte-based lookup from 2014 can be improved by using `string.Create()`, which uses a `Span` behind the scenes to reduces some copying. See: https://dotnetfiddle.net/LoKMV9 Not sure how it would benchmark, though. – Joel Coehoorn Jun 14 '23 at 19:45
There's a class called SoapHexBinary that does exactly what you want.
using System.Runtime.Remoting.Metadata.W3cXsd2001;
public static byte[] GetStringToBytes(string value)
{
SoapHexBinary shb = SoapHexBinary.Parse(value);
return shb.Value;
}
public static string GetBytesToString(byte[] value)
{
SoapHexBinary shb = new SoapHexBinary(value);
return shb.ToString();
}
-
40SoapHexBinary is available from .NET 1.0 and is in mscorlib. Despite it's funny namespace, it does exactly what the question asked. – Sly Gryphon Jun 28 '11 at 06:48
-
4Great find! Note that you will need to pad odd strings with a leading 0 for GetStringToBytes, like the other solution. – Carter Medlin Oct 31 '11 at 17:10
-
Have you seen the implementation thought? The accepted answer has a better one IMHO. – mfloryan Jan 26 '12 at 13:42
-
Do you mean the implementation of SoapHexBinary? If so what does it do that makes it worse than the implementation in the accepted answer? – Mykroft Jan 26 '12 at 19:20
-
7Interesting to see the Mono implementation here: https://github.com/mono/mono/blob/master/mcs/class/corlib/System.Runtime.Remoting.Metadata.W3cXsd2001/SoapHexBinary.cs – Jeremy Apr 29 '12 at 04:40
-
In my tests (which I'm about to dump into an answer), Mono's impl. is about 10% faster than `SoapHexBinary`, and 16x slower than mine... – Ben Mosher May 22 '12 at 16:39
-
12
-
Yeah that's pretty unfortunate (that's not in .net core). Was pretty good till today – Gaspa79 May 04 '21 at 18:01
When writing crypto code it's common to avoid data dependent branches and table lookups to ensure the runtime doesn't depend on the data, since data dependent timing can lead to side-channel attacks.
It's also pretty fast.
static string ByteToHexBitFiddle(byte[] bytes)
{
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);
}
Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn
Abandon all hope, ye who enter here
An explanation of the weird bit fiddling:
bytes[i] >> 4
extracts the high nibble of a byte
bytes[i] & 0xF
extracts the low nibble of a byteb - 10
is< 0
for valuesb < 10
, which will become a decimal digit
is>= 0
for valuesb > 10
, which will become a letter fromA
toF
.- Using
i >> 31
on a signed 32 bit integer extracts the sign, thanks to sign extension. It will be-1
fori < 0
and0
fori >= 0
. - Combining 2) and 3), shows that
(b-10)>>31
will be0
for letters and-1
for digits. - Looking at the case for letters, the last summand becomes
0
, andb
is in the range 10 to 15. We want to map it toA
(65) toF
(70), which implies adding 55 ('A'-10
). - Looking at the case for digits, we want to adapt the last summand so it maps
b
from the range 0 to 9 to the range0
(48) to9
(57). This means it needs to become -7 ('0' - 55
).
Now we could just multiply with 7. But since -1 is represented by all bits being 1, we can instead use& -7
since(0 & -7) == 0
and(-1 & -7) == -7
.
Some further considerations:
- I didn't use a second loop variable to index into
c
, since measurement shows that calculating it fromi
is cheaper. - Using exactly
i < bytes.Length
as upper bound of the loop allows the JITter to eliminate bounds checks onbytes[i]
, so I chose that variant. - Making
b
an int allows unnecessary conversions from and to byte.
The same thing can be implemented using the new string.Create
function, which avoids having to allocate a separate char[]
array.
- We can also factor out the conversion of each nibble into a function.
- Adding
AggressiveInlining
should allow that function to disappear from the JIT. - We can adjust by
32
to get a lower-case result. - We can also use
Memory<byte>
instead of an array, this allows a wider range of memory buffers (including arrays).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static string ByteToHexBitFiddle(Memory<byte> bytes, bool lowercase = false) =>
lowercase
? string.Create(bytes.Length * 2, bytes, LowercaseFillHex)
: string.Create(bytes.Length * 2, bytes, UppercaseFillHex);
static void UppercaseFillHex(Span<char> span, Memory<byte> mem)
{
var bytes = mem.Span;
for (int i = 0; i < bytes.Length; i++)
{
span[i * 2] = ConvertNibble(bytes[i] >> 4, 0);
span[i * 2 + 1] = ConvertNibble(bytes[i] & 0xF, 0);
}
}
static void LowercaseFillHex(Span<char> span, Memory<byte> mem)
{
var bytes = mem.Span;
for (int i = 0; i < bytes.Length; i++)
{
span[i * 2] = ConvertNibble(bytes[i] >> 4, 32);
span[i * 2 + 1] = ConvertNibble(bytes[i] & 0xF, 32);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static char ConvertNibble(int nibble, int adjust) =>
(char)(55 + adjust + nibble + (((nibble - 10) >> 31) & (-7 - adjust)));

- 52,284
- 6
- 19
- 43

- 106,488
- 23
- 218
- 262
-
12
-
18+1 for properly citing your source after invoking that bit of black magic. All hail Cthulhu. – Edward Aug 02 '13 at 20:41
-
-
6
-
What I meant to say is, if I have "0x1B", how do I convert that to byte? – Syaiful Nizam Yahya Nov 07 '13 at 03:59
-
10Nice! For those who need lowercase output, the expression obviously changes to `87 + b + (((b-10)>>31)&-39)` – eXavier Jan 06 '14 at 17:36
-
I've now got it in Java and C# for both encoding and decoding to show off my "dark magic" (single loop, no branches except for a final one for hex mistakes). Separate from your concise code of course, I like to puzzle sometimes. – Maarten Bodewes Jan 20 '14 at 23:17
-
-
@CoolOppo, I'm not sure what you mean but a Hex string is in `"123456789ABCDEF"` format, meaning each two character converts to `one byte` – AaA Jun 10 '15 at 02:59
-
2@AaA You said "`byte[] array`", which literally means an array of byte arrays, or `byte[][]`. I was just poking fun. – CoolOppo Jun 10 '15 at 03:09
-
Why not just use `'A'-0xA` instead of `55`? Let the compiler figure it out; magic numbers like `55` are not readily understandable by humans, and are prone to error. – David R Tribble Jul 16 '18 at 15:44
-
@DavidRTribble Because I do not want to incur Great Lord Cthulhu's wreath. – CodesInChaos Jul 16 '18 at 17:43
-
@CodesInChaos - I know you meant *wrath*. But see, that's a problem with a `101` character, which I of course would write as `'e'`. ;-) – David R Tribble Jul 23 '18 at 15:47
-
@CodesInChaos Amazing, never though that minus is playing such a game in 'and' operator, never used it. Seems like magic. What is background ? By my opinion, minus in such expression should mean left-most bit is set and ordinary 'and' should give minus if source is only minus, but its different: 3 & 9 == 1, its ok, but 3 & -9 == 3, and (-3) & (-9) == -11, whats the fuck ? – Oleg Skripnyak May 25 '19 at 08:41
-
If you want more flexibility than BitConverter
, but don't want those clunky 1990s-style explicit loops, then you can do:
String.Join(String.Empty, Array.ConvertAll(bytes, x => x.ToString("X2")));
Or, if you're using .NET 4.0:
String.Concat(Array.ConvertAll(bytes, x => x.ToString("X2")));
(The latter from a comment on the original post.)

- 30,738
- 21
- 105
- 131

- 39,055
- 11
- 90
- 118
-
23Even shorter: String.Concat(Array.ConvertAll(bytes, x => x.ToString("X2")) – Nestor Nov 25 '09 at 15:04
-
-
18Even shorter: String.Concat(bytes.Select(b => b.ToString("X2"))) _[.NET4]_ – Allon Guralnek Jun 16 '11 at 06:39
-
14
-
2
-
5those "90's style" loops are generally faster, but by a negligible enough amount that it wont matter in most contexts. Still worth mentioning though – Austin_Anderson Oct 24 '17 at 19:47
Another lookup table based approach. This one uses only one lookup table for each byte, instead of a lookup table per nibble.
private static readonly uint[] _lookup32 = CreateLookup32();
private static uint[] CreateLookup32()
{
var result = new uint[256];
for (int i = 0; i < 256; i++)
{
string s=i.ToString("X2");
result[i] = ((uint)s[0]) + ((uint)s[1] << 16);
}
return result;
}
private static string ByteArrayToHexViaLookup32(byte[] bytes)
{
var lookup32 = _lookup32;
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);
}
I also tested variants of this using ushort
, struct{char X1, X2}
, struct{byte X1, X2}
in the lookup table.
Depending on the compilation target (x86, X64) those either had the approximately same performance or were slightly slower than this variant.
And for even higher performance, its unsafe
sibling:
private static readonly uint[] _lookup32Unsafe = CreateLookup32Unsafe();
private static readonly uint* _lookup32UnsafeP = (uint*)GCHandle.Alloc(_lookup32Unsafe,GCHandleType.Pinned).AddrOfPinnedObject();
private static uint[] CreateLookup32Unsafe()
{
var result = new uint[256];
for (int i = 0; i < 256; i++)
{
string s=i.ToString("X2");
if(BitConverter.IsLittleEndian)
result[i] = ((uint)s[0]) + ((uint)s[1] << 16);
else
result[i] = ((uint)s[1]) + ((uint)s[0] << 16);
}
return result;
}
public static string ByteArrayToHexViaLookup32Unsafe(byte[] bytes)
{
var lookupP = _lookup32UnsafeP;
var result = new char[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 new string(result);
}
Or if you consider it acceptable to write into the string directly:
public static string ByteArrayToHexViaLookup32UnsafeDirect(byte[] bytes)
{
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;
}

- 106,488
- 23
- 218
- 262
-
1Why does creating the lookup table in the unsafe version swap the nibbles of the precomputed byte ? I thought endianness only changed ordering of entities that were formed of multiple bytes. – Raif Atef Nov 05 '14 at 13:13
-
1@RaifAtef What matters here isn't the order of the nibbles. But the order of 16 bit words in a 32 bit integer. But I'm considering rewriting it so the same code can run regardless of endianness. – CodesInChaos Nov 07 '14 at 12:09
-
1Re-reading the code, I think that you did this because when you cast the char* later to a uint* and assign it (when generating the hex char), the runtime/CPU will flip the bytes (since uint is not treated the same as 2 separate 16-bit chars) so you're pre-flipping them to compensate. Am I right ? Endianness is confusing :-). – Raif Atef Nov 07 '14 at 13:26
-
2All right, I'll bite -- what advantage is there to pinning `_lookup32Unsafe` indefinitely instead of just doing a third `fixed` statement and letting the GC relocate the array to its heart's content whenever this method isn't running? – Joe Amenta Jan 09 '16 at 12:24
-
1@JoeAmenta Not sure if there is any measurable advantage is this case. Perhaps I simply didn't think about that alternative when writing this code. – CodesInChaos Jan 09 '16 at 15:32
-
7This just answer half of the question... How about from hex string to bytes? – Narvalex Mar 08 '17 at 17:28
-
1Why does only the unsafe variant check endianness? The first example seems to just assume the machine is littleEndian. – TamaMcGlinn Jan 30 '18 at 10:33
-
1@TamaMcGlinn The safe implementation uses little-endian for its internal representation, but makes no assumptions about machine endianness. So it should still work on big-endian machines (possibly a bit slower). By contrast the unsafe variant re-interprets two 16-bit words as one 32-bit word, which is why it has to be careful about host endianness. – CodesInChaos Jan 30 '18 at 11:00
-
8@CodesInChaos I wonder if `Span` can be used now instead of `unsafe` ?? – Konrad Dec 04 '19 at 13:12
-
1@Konrad it probably could; one interesting impact is that at this point a `static ReadOnlySpan` that is initialized with an array of literals becomes static data in the DLL. – to11mtm Jan 16 '21 at 22:37
-
There's also `string.Create()`, which manages the Span for you. (See here: https://dotnetfiddle.net/ok6Vld ) – Joel Coehoorn Jun 14 '23 at 19:33
-
Ah, just found this, so obviously you've seen it ;) https://stackoverflow.com/a/14333437/3043 – Joel Coehoorn Jun 14 '23 at 19:55
You can use the BitConverter.ToString method:
byte[] bytes = {0, 1, 2, 4, 8, 16, 32, 64, 128, 255};
Console.WriteLine( BitConverter.ToString(bytes));
Output:
00-01-02-04-08-10-20-40-80-FF
More information: BitConverter.ToString Method (Byte[])
I just encountered the very same problem today, and I came across this code:
private static string ByteArrayToHex(byte[] barray)
{
char[] c = new char[barray.Length * 2];
byte b;
for (int i = 0; i < barray.Length; ++i)
{
b = ((byte)(barray[i] >> 4));
c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
b = ((byte)(barray[i] & 0xF));
c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
}
return new string(c);
}
Source: Forum post byte[] Array to Hex String (see the post by PZahra). I modified the code a little to remove the 0x prefix.
I did some performance testing to the code and it was almost eight times faster than using BitConverter.ToString() (the fastest according to patridge's post).

- 30,738
- 21
- 105
- 131

- 10,283
- 16
- 60
- 82
-
not to mention that this uses the least memory. No intermediate strings created whatsoever. – Chochos Oct 16 '09 at 17:36
-
9
-
This is great because it works on basically any version of NET, including NETMF. A winner! – Jonesome Reinstate Monica Feb 06 '12 at 04:26
-
2The accepted answer provides 2 excellent HexToByteArray methods, which represent the other half of the question. Waleed's solution answers the running question of how to do this without creating a huge number of strings in the process. – Brendten Eickstaedt Oct 10 '12 at 16:08
-
Does new string(c) copy and re-allocate or is it smart enough to know when it can simply wrap the char[]? – jjxtra Oct 15 '13 at 17:24
-
@PsychoDad, it copies. The string needs to be immutable while the char[] may change after the string is created. – Brian Reichle Jan 21 '14 at 06:51
-
@SlyGryphon Actually if you scroll down from the mentioned forum post, I actually provided the other side of that, which has since been updated here: https://stackoverflow.com/a/22158486/278889 – Patrick Sep 27 '18 at 14:59
As of .NET 5 RC2 you can use:
Convert.ToHexString(byte[] inArray)
which returns astring
andConvert.FromHexString(string s)
which returns abyte[]
.
Overloads are available that take span parameters.

- 188,800
- 56
- 490
- 992

- 585
- 6
- 7
-
3In .NET 6, `Convert.ToHexString` uses SSSE3 instruction set on CPU, so it is not only convenient to use as in .NET 5, but also more [performant](https://github.com/dotnet/runtime/pull/44111) for inputs more than 3 bytes. Performance difference is more clear as the input size increases. – MÇT Aug 26 '21 at 09:04
This is an answer to revision 4 of Tomalak's highly popular answer (and subsequent edits).
I'll make the case that this edit is wrong, and explain why it could be reverted. Along the way, you might learn a thing or two about some internals, and see yet another example of what premature optimization really is and how it can bite you.
tl;dr: Just use Convert.ToByte
and String.Substring
if you're in a hurry ("Original code" below), it's the best combination if you don't want to re-implement Convert.ToByte
. Use something more advanced (see other answers) that doesn't use Convert.ToByte
if you need performance. Do not use anything else other than String.Substring
in combination with Convert.ToByte
, unless someone has something interesting to say about this in the comments of this answer.
warning: This answer may become obsolete if a Convert.ToByte(char[], Int32)
overload is implemented in the framework. This is unlikely to happen soon.
As a general rule, I don't much like to say "don't optimize prematurely", because nobody knows when "premature" is. The only thing you must consider when deciding whether to optimize or not is: "Do I have the time and resources to investigate optimization approaches properly?". If you don't, then it's too soon, wait until your project is more mature or until you need the performance (if there is a real need, then you will make the time). In the meantime, do the simplest thing that could possibly work instead.
Original code:
public static byte[] HexadecimalStringToByteArray_Original(string input)
{
var outputLength = input.Length / 2;
var output = new byte[outputLength];
for (var i = 0; i < outputLength; i++)
output[i] = Convert.ToByte(input.Substring(i * 2, 2), 16);
return output;
}
Revision 4:
public static byte[] HexadecimalStringToByteArray_Rev4(string input)
{
var outputLength = input.Length / 2;
var output = new byte[outputLength];
using (var sr = new StringReader(input))
{
for (var i = 0; i < outputLength; i++)
output[i] = Convert.ToByte(new string(new char[2] { (char)sr.Read(), (char)sr.Read() }), 16);
}
return output;
}
The revision avoids String.Substring
and uses a StringReader
instead. The given reason is:
Edit: you can improve performance for long strings by using a single pass parser, like so:
Well, looking at the reference code for String.Substring
, it's clearly "single-pass" already; and why shouldn't it be? It operates at byte-level, not on surrogate pairs.
It does allocate a new string however, but then you need to allocate one to pass to Convert.ToByte
anyway. Furthermore, the solution provided in the revision allocates yet another object on every iteration (the two-char array); you can safely put that allocation outside the loop and reuse the array to avoid that.
public static byte[] HexadecimalStringToByteArray(string input)
{
var outputLength = input.Length / 2;
var output = new byte[outputLength];
var numeral = new char[2];
using (var sr = new StringReader(input))
{
for (var i = 0; i < outputLength; i++)
{
numeral[0] = (char)sr.Read();
numeral[1] = (char)sr.Read();
output[i] = Convert.ToByte(new string(numeral), 16);
}
}
return output;
}
Each hexadecimal numeral
represents a single octet using two digits (symbols).
But then, why call StringReader.Read
twice? Just call its second overload and ask it to read two characters in the two-char array at once; and reduce the amount of calls by two.
public static byte[] HexadecimalStringToByteArray(string input)
{
var outputLength = input.Length / 2;
var output = new byte[outputLength];
var numeral = new char[2];
using (var sr = new StringReader(input))
{
for (var i = 0; i < outputLength; i++)
{
var read = sr.Read(numeral, 0, 2);
Debug.Assert(read == 2);
output[i] = Convert.ToByte(new string(numeral), 16);
}
}
return output;
}
What you're left with is a string reader whose only added "value" is a parallel index (internal _pos
) which you could have declared yourself (as j
for example), a redundant length variable (internal _length
), and a redundant reference to the input string (internal _s
). In other words, it's useless.
If you wonder how Read
"reads", just look at the code, all it does is call String.CopyTo
on the input string. The rest is just book-keeping overhead to maintain values we don't need.
So, remove the string reader already, and call CopyTo
yourself; it's simpler, clearer, and more efficient.
public static byte[] HexadecimalStringToByteArray(string input)
{
var outputLength = input.Length / 2;
var output = new byte[outputLength];
var numeral = new char[2];
for (int i = 0, j = 0; i < outputLength; i++, j += 2)
{
input.CopyTo(j, numeral, 0, 2);
output[i] = Convert.ToByte(new string(numeral), 16);
}
return output;
}
Do you really need a j
index that increments in steps of two parallel to i
? Of course not, just multiply i
by two (which the compiler should be able to optimize to an addition).
public static byte[] HexadecimalStringToByteArray_BestEffort(string input)
{
var outputLength = input.Length / 2;
var output = new byte[outputLength];
var numeral = new char[2];
for (int i = 0; i < outputLength; i++)
{
input.CopyTo(i * 2, numeral, 0, 2);
output[i] = Convert.ToByte(new string(numeral), 16);
}
return output;
}
What does the solution look like now? Exactly like it was at the beginning, only instead of using String.Substring
to allocate the string and copy the data to it, you're using an intermediary array to which you copy the hexadecimal numerals to, then allocate the string yourself and copy the data again from the array and into the string (when you pass it in the string constructor). The second copy might be optimized-out if the string is already in the intern pool, but then String.Substring
will also be able to avoid it in these cases.
In fact, if you look at String.Substring
again, you see that it uses some low-level internal knowledge of how strings are constructed to allocate the string faster than you could normally do it, and it inlines the same code used by CopyTo
directly in there to avoid the call overhead.
String.Substring
- Worst-case: One fast allocation, one fast copy.
- Best-case: No allocation, no copy.
Manual method
- Worst-case: Two normal allocations, one normal copy, one fast copy.
- Best-case: One normal allocation, one normal copy.
Conclusion? If you want to use Convert.ToByte(String, Int32)
(because you don't want to re-implement that functionality yourself), there doesn't seem to be a way to beat String.Substring
; all you do is run in circles, re-inventing the wheel (only with sub-optimal materials).
Note that using Convert.ToByte
and String.Substring
is a perfectly valid choice if you don't need extreme performance. Remember: only opt for an alternative if you have the time and resources to investigate how it works properly.
If there was a Convert.ToByte(char[], Int32)
, things would be different of course (it would be possible to do what I described above and completely avoid String
).
I suspect that people who report better performance by "avoiding String.Substring
" also avoid Convert.ToByte(String, Int32)
, which you should really be doing if you need the performance anyway. Look at the countless other answers to discover all the different approaches to do that.
Disclaimer: I haven't decompiled the latest version of the framework to verify that the reference source is up-to-date, I assume it is.
Now, it all sounds good and logical, hopefully even obvious if you've managed to get so far. But is it true?
Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
Cores: 8
Current Clock Speed: 2600
Max Clock Speed: 2600
--------------------
Parsing hexadecimal string into an array of bytes
--------------------
HexadecimalStringToByteArray_Original: 7,777.09 average ticks (over 10000 runs), 1.2X
HexadecimalStringToByteArray_BestEffort: 8,550.82 average ticks (over 10000 runs), 1.1X
HexadecimalStringToByteArray_Rev4: 9,218.03 average ticks (over 10000 runs), 1.0X
Yes!
Props to Partridge for the bench framework, it's easy to hack. The input used is the following SHA-1 hash repeated 5000 times to make a 100,000 bytes long string.
209113288F93A9AB8E474EA78D899AFDBB874355
Have fun! (But optimize with moderation.)
Dotnet 5 Update
To convert from byte[]
(byte array) to hexadecimal string
, use:
System.Convert.ToHexString
var myBytes = new byte[100];
var myString = System.Convert.ToHexString(myBytes);
To convert from hexadecimal string
to byte[]
, use:
System.Convert.FromHexString
var myString = "E10B116E8530A340BCC7B3EAC208487B";
var myBytes = System.Convert.FromHexString(myString);

- 627
- 8
- 19
Converting byte[] to a hexadecimal string - benchmark / 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.
Remember that micro-benchmarking won't ever represent the actual situation, and you should do your 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. Still, 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 change a parameter X2
to x2
or change the letters from uppercase to lowercase in a mapping.
Leaderboard
It is 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 undoubtedly the fastest out there, and in my perspective, it should always be used if you have the option - it's swift and 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.
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 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 for this one. LookupSpanPerByteSpan
is also very fast, but the code size 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 creation. However, I believe 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 memory allocations. [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);
}
Source code
The source code for all methods, the benchmark, and this answer can be found here as a Gist on my GitHub.

- 966
- 4
- 18
- 36
-
1The ToHexString method is very useful. It seems FromHexString does the reverse operation – user4779 Jun 20 '22 at 15:30
-
Why do I get the compile error `cannot convert 'System.Span
' to 'char*'` on `return new string(res)`? Thanks – Rui Caramalho Jun 13 '23 at 14:21 -
@RuiCaramalho Since you're using .Net Framework and not .NET you should use `res.ToString()` instead of `new string(res)`. You might also need to install NuGet package `System.Memory` to support `System.Span`. Be aware that this code was tested using .NET and not .Net Framework so there might (or might not) be some difference in the performance. – antoninkriz Jun 13 '23 at 17:25
-
Complement to answer by @CodesInChaos (reversed method)
public static byte[] HexToByteUsingByteManipulation(string s)
{
byte[] bytes = new byte[s.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
int hi = s[i*2] - 65;
hi = hi + 10 + ((hi >> 31) & 7);
int lo = s[i*2 + 1] - 65;
lo = lo + 10 + ((lo >> 31) & 7) & 0x0f;
bytes[i] = (byte) (lo | hi << 4);
}
return bytes;
}
Explanation:
& 0x0f
is to support also lower case letters
hi = hi + 10 + ((hi >> 31) & 7);
is the same as:
hi = ch-65 + 10 + (((ch-65) >> 31) & 7);
For '0'..'9' it is the same as hi = ch - 65 + 10 + 7;
which is hi = ch - 48
(this is because of 0xffffffff & 7
).
For 'A'..'F' it is hi = ch - 65 + 10;
(this is because of 0x00000000 & 7
).
For 'a'..'f' we have to big numbers so we must subtract 32 from default version by making some bits 0
by using & 0x0f
.
65 is code for 'A'
48 is code for '0'
7 is the number of letters between '9'
and 'A'
in the ASCII table (...456789:;<=>?@ABCD...
).

- 30,738
- 21
- 105
- 131

- 2,413
- 2
- 21
- 26
This problem could also be solved using a look-up table. This would require a small amount of static memory for both the encoder and decoder. This method will however be fast:
- Encoder table 512 bytes or 1024 bytes (twice the size if both upper and lower case is needed)
- Decoder table 256 bytes or 64 KiB (either a single char look-up or dual char look-up)
My solution uses 1024 bytes for the encoding table, and 256 bytes for decoding.
Decoding
private static readonly byte[] LookupTable = new byte[] {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
private static byte Lookup(char c)
{
var b = LookupTable[c];
if (b == 255)
throw new IOException("Expected a hex character, got " + c);
return b;
}
public static byte ToByte(char[] chars, int offset)
{
return (byte)(Lookup(chars[offset]) << 4 | Lookup(chars[offset + 1]));
}
Encoding
private static readonly char[][] LookupTableUpper;
private static readonly char[][] LookupTableLower;
static Hex()
{
LookupTableLower = new char[256][];
LookupTableUpper = new char[256][];
for (var i = 0; i < 256; i++)
{
LookupTableLower[i] = i.ToString("x2").ToCharArray();
LookupTableUpper[i] = i.ToString("X2").ToCharArray();
}
}
public static char[] ToCharLower(byte[] b, int bOffset)
{
return LookupTableLower[b[bOffset]];
}
public static char[] ToCharUpper(byte[] b, int bOffset)
{
return LookupTableUpper[b[bOffset]];
}
Comparison
StringBuilderToStringFromBytes: 106148
BitConverterToStringFromBytes: 15783
ArrayConvertAllToStringFromBytes: 54290
ByteManipulationToCharArray: 8444
TableBasedToCharArray: 5651 *
* this solution
Note
During decoding IOException and IndexOutOfRangeException could occur (if a character has a too high value > 256). Methods for de/encoding streams or arrays should be implemented, this is just a proof of concept.

- 30,738
- 21
- 105
- 131

- 317
- 3
- 4
Why make it complex? This is simple in Visual Studio 2008:
C#:
string hex = BitConverter.ToString(YourByteArray).Replace("-", "");
VB:
Dim hex As String = BitConverter.ToString(YourByteArray).Replace("-", "")

- 30,738
- 21
- 105
- 131

- 49
- 1
- 2
This is a great post. I like Waleed's solution. I haven't run it through patridge's test but it seems to be quite fast. I also needed the reverse process, converting a hex string to a byte array, so I wrote it as a reversal of Waleed's solution. Not sure if it's any faster than Tomalak's original solution. Again, I did not run the reverse process through patridge's test either.
private byte[] HexStringToByteArray(string hexString)
{
int hexStringLength = hexString.Length;
byte[] b = new byte[hexStringLength / 2];
for (int i = 0; i < hexStringLength; i += 2)
{
int topChar = (hexString[i] > 0x40 ? hexString[i] - 0x37 : hexString[i] - 0x30) << 4;
int bottomChar = hexString[i + 1] > 0x40 ? hexString[i + 1] - 0x37 : hexString[i + 1] - 0x30;
b[i / 2] = Convert.ToByte(topChar + bottomChar);
}
return b;
}

- 2,886
- 2
- 28
- 33
-
1This code assumes the hex string uses upper case alpha chars, and blows up if the hex string uses lower case alpha. Might want to do a "uppercase" conversion on the input string to be safe. – Marc Novakowski Jan 26 '10 at 19:17
-
That's an astute observation Marc. The code was written to reverse Waleed's solution. The ToUpper call would slow down the algorithm some, but would allow it to handle lower case alpha chars. – Chris F Jan 26 '10 at 20:27
-
3Convert.ToByte(topChar + bottomChar) can be written as (byte)(topChar + bottomChar) – Amir Rezaei Feb 12 '11 at 21:17
-
To handle both cases without a large performance penalty, `hexString[i] &= ~0x20;` – Ben Voigt Jul 31 '14 at 22:31
Not to pile on to the many answers here, but I found a fairly optimal (~4.5x better than accepted), straightforward implementation of the hex string parser. First, output from my tests (the first batch is my implementation):
Give me that string:
04c63f7842740c77e545bb0b2ade90b384f119f6ab57b680b7aa575a2f40939f
Time to parse 100,000 times: 50.4192 ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
Accepted answer: (StringToByteArray)
Time to parse 100000 times: 233.1264ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
With Mono's implementation:
Time to parse 100000 times: 777.2544ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
With SoapHexBinary:
Time to parse 100000 times: 845.1456ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
The base64 and 'BitConverter'd' lines are there to test for correctness. Note that they are equal.
The implementation:
public static byte[] ToByteArrayFromHex(string hexString)
{
if (hexString.Length % 2 != 0) throw new ArgumentException("String must have an even length");
var array = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
array[i/2] = ByteFromTwoChars(hexString[i], hexString[i + 1]);
}
return array;
}
private static byte ByteFromTwoChars(char p, char p_2)
{
byte ret;
if (p <= '9' && p >= '0')
{
ret = (byte) ((p - '0') << 4);
}
else if (p <= 'f' && p >= 'a')
{
ret = (byte) ((p - 'a' + 10) << 4);
}
else if (p <= 'F' && p >= 'A')
{
ret = (byte) ((p - 'A' + 10) << 4);
} else throw new ArgumentException("Char is not a hex digit: " + p,"p");
if (p_2 <= '9' && p_2 >= '0')
{
ret |= (byte) ((p_2 - '0'));
}
else if (p_2 <= 'f' && p_2 >= 'a')
{
ret |= (byte) ((p_2 - 'a' + 10));
}
else if (p_2 <= 'F' && p_2 >= 'A')
{
ret |= (byte) ((p_2 - 'A' + 10));
} else throw new ArgumentException("Char is not a hex digit: " + p_2, "p_2");
return ret;
}
I tried some stuff with unsafe
and moving the (clearly redundant) character-to-nibble if
sequence to another method, but this was the fastest it got.
(I concede that this answers half the question. I felt that the string->byte[] conversion was underrepresented, while the byte[]->string angle seems to be well covered. Thus, this answer.)

- 30,738
- 21
- 105
- 131

- 13,251
- 7
- 69
- 80
-
1For the followers of Knuth: I did this because I need to parse a few thousand hex strings every few minutes or so, so it's important that it be as fast as possible (in the inner loop, as it were). Tomalak's solution is not notably slower if many such parses are not occurring. – Ben Mosher May 22 '12 at 17:01
Safe versions:
public static class HexHelper
{
[System.Diagnostics.Contracts.Pure]
public static string ToHex(this byte[] value)
{
if (value == null)
throw new ArgumentNullException("value");
const string hexAlphabet = @"0123456789ABCDEF";
var chars = new char[checked(value.Length * 2)];
unchecked
{
for (int i = 0; i < value.Length; i++)
{
chars[i * 2] = hexAlphabet[value[i] >> 4];
chars[i * 2 + 1] = hexAlphabet[value[i] & 0xF];
}
}
return new string(chars);
}
[System.Diagnostics.Contracts.Pure]
public static byte[] FromHex(this string value)
{
if (value == null)
throw new ArgumentNullException("value");
if (value.Length % 2 != 0)
throw new ArgumentException("Hexadecimal value length must be even.", "value");
unchecked
{
byte[] result = new byte[value.Length / 2];
for (int i = 0; i < result.Length; i++)
{
// 0(48) - 9(57) -> 0 - 9
// A(65) - F(70) -> 10 - 15
int b = value[i * 2]; // High 4 bits.
int val = ((b - '0') + ((('9' - b) >> 31) & -7)) << 4;
b = value[i * 2 + 1]; // Low 4 bits.
val += (b - '0') + ((('9' - b) >> 31) & -7);
result[i] = checked((byte)val);
}
return result;
}
}
}
Unsafe versions For those who prefer performance and do not afraid of unsafeness. About 35% faster ToHex and 10% faster FromHex.
public static class HexUnsafeHelper
{
[System.Diagnostics.Contracts.Pure]
public static unsafe string ToHex(this byte[] value)
{
if (value == null)
throw new ArgumentNullException("value");
const string alphabet = @"0123456789ABCDEF";
string result = new string(' ', checked(value.Length * 2));
fixed (char* alphabetPtr = alphabet)
fixed (char* resultPtr = result)
{
char* ptr = resultPtr;
unchecked
{
for (int i = 0; i < value.Length; i++)
{
*ptr++ = *(alphabetPtr + (value[i] >> 4));
*ptr++ = *(alphabetPtr + (value[i] & 0xF));
}
}
}
return result;
}
[System.Diagnostics.Contracts.Pure]
public static unsafe byte[] FromHex(this string value)
{
if (value == null)
throw new ArgumentNullException("value");
if (value.Length % 2 != 0)
throw new ArgumentException("Hexadecimal value length must be even.", "value");
unchecked
{
byte[] result = new byte[value.Length / 2];
fixed (char* valuePtr = value)
{
char* valPtr = valuePtr;
for (int i = 0; i < result.Length; i++)
{
// 0(48) - 9(57) -> 0 - 9
// A(65) - F(70) -> 10 - 15
int b = *valPtr++; // High 4 bits.
int val = ((b - '0') + ((('9' - b) >> 31) & -7)) << 4;
b = *valPtr++; // Low 4 bits.
val += (b - '0') + ((('9' - b) >> 31) & -7);
result[i] = checked((byte)val);
}
}
return result;
}
}
}
BTW For benchmark testing initializing alphabet every time convert function called is wrong, alphabet must be const (for string) or static readonly (for char[]). Then alphabet-based conversion of byte[] to string becomes as fast as byte manipulation versions.
And of course test must be compiled in Release (with optimization) and with debug option "Suppress JIT optimization" turned off (same for "Enable Just My Code" if code must be debuggable).

- 21
- 1
- 3
From Microsoft's developers, a nice, simple conversion:
public static string ByteArrayToString(byte[] ba)
{
// Concatenate the bytes into one long string
return ba.Aggregate(new StringBuilder(32),
(sb, b) => sb.Append(b.ToString("X2"))
).ToString();
}
While the above is clean and compact, performance junkies will scream about it using enumerators. You can get peak performance with an improved version of Tomalak's original answer:
public static string ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);
for(int i=0; i < ba.Length; i++) // <-- Use for loop is faster than foreach
hex.Append(ba[i].ToString("X2")); // <-- ToString is faster than AppendFormat
return hex.ToString();
}
This is the fastest of all the routines I've seen posted here so far. Don't just take my word for it... performance test each routine and inspect its CIL code for yourself.
-
3The iterator is not the main problem of this code. You should benchmark `b.ToSting("X2")`. – dolmen Aug 20 '13 at 23:49
Extension methods (disclaimer: completely untested code, BTW...):
public static class ByteExtensions
{
public static string ToHexString(this byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);
foreach (byte b in ba)
{
hex.AppendFormat("{0:x2}", b);
}
return hex.ToString();
}
}
etc.. Use either of Tomalak's three solutions (with the last one being an extension method on a string).

- 30,738
- 21
- 105
- 131

- 84,693
- 113
- 396
- 647
-
1You should probably test the code before you offer it up for a question like this. – jww Feb 16 '17 at 19:08
Inverse function for Waleed Eissa code (Hex String To Byte Array):
public static byte[] HexToBytes(this string hexString)
{
byte[] b = new byte[hexString.Length / 2];
char c;
for (int i = 0; i < hexString.Length / 2; i++)
{
c = hexString[i * 2];
b[i] = (byte)((c < 0x40 ? c - 0x30 : (c < 0x47 ? c - 0x37 : c - 0x57)) << 4);
c = hexString[i * 2 + 1];
b[i] += (byte)(c < 0x40 ? c - 0x30 : (c < 0x47 ? c - 0x37 : c - 0x57));
}
return b;
}
Waleed Eissa function with lower case support:
public static string BytesToHex(this byte[] barray, bool toLowerCase = true)
{
byte addByte = 0x37;
if (toLowerCase) addByte = 0x57;
char[] c = new char[barray.Length * 2];
byte b;
for (int i = 0; i < barray.Length; ++i)
{
b = ((byte)(barray[i] >> 4));
c[i * 2] = (char)(b > 9 ? b + addByte : b + 0x30);
b = ((byte)(barray[i] & 0xF));
c[i * 2 + 1] = (char)(b > 9 ? b + addByte : b + 0x30);
}
return new string(c);
}

- 2,274
- 23
- 23
Fastest method for old school people... miss you pointers
static public byte[] HexStrToByteArray(string str)
{
byte[] res = new byte[(str.Length % 2 != 0 ? 0 : str.Length / 2)]; //check and allocate memory
for (int i = 0, j = 0; j < res.Length; i += 2, j++) //convert loop
res[j] = (byte)((str[i] % 32 + 9) % 25 * 16 + (str[i + 1] % 32 + 9) % 25);
return res;
}
.NET 5 has added the Convert.ToHexString method.
For those using an older version of .NET
internal static class ByteArrayExtensions
{
public static string ToHexString(this byte[] bytes, Casing casing = Casing.Upper)
{
Span<char> result = stackalloc char[0];
if (bytes.Length > 16)
{
var array = new char[bytes.Length * 2];
result = array.AsSpan();
}
else
{
result = stackalloc char[bytes.Length * 2];
}
int pos = 0;
foreach (byte b in bytes)
{
ToCharsBuffer(b, result, pos, casing);
pos += 2;
}
return result.ToString();
}
private 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' ].
Lower = 0x2020U,
}
Adapted from the .NET repository https://github.com/dotnet/runtime/blob/v5.0.3/src/libraries/System.Private.CoreLib/src/System/Convert.cs https://github.com/dotnet/runtime/blob/v5.0.3/src/libraries/Common/src/System/HexConverter.cs

- 1,329
- 2
- 17
- 23
Tests: Hex String To Byte Array
I noticed that most of tests were performed on functions that convert Bytes array to Hex string. So, in this post I will focus on the other side: functions that convert Hex String To Byte Array. If you are interested in result only, you could skip down to Summary section. The test code file is supplied at the end of the post.
Labels
I would like to name the function from the accepted answer (by Tomalak) StringToByteArrayV1, or to shortcut it to V1. rest of functions will be named in same way: V2, V3, V4, ..., etc.
Index of Participating Functions
- StringToByteArrayV1 by Tomalak (the accepted answer)
- StringToByteArrayV2 by Mykroft (using SoapHexBinary)
- StringToByteArrayV3 by drphrozen (look-up table)
- StringToByteArrayV4 by CoperNick (Byte Manipulation)
- StringToByteArrayV5_1 by Chris F (Byte Manipulation)
- StringToByteArrayV5_2 by Chris F ( V5_1 + ehanced it based on Amir Rezaei's comment)
- StringToByteArrayV5_3 by Chris F ( V5_2 + ehanced it based on Ben Voigt's comment) (you could see final shape of it at this post published test code)
- StringToByteArrayV6 by Ben Mosher (Byte Manipulation)
- StringToByteArrayV7 by Maratius (Byte Manipulation - the safe version)
- StringToByteArrayV8 by Maratius (Byte Manipulation - the unsafe version)
- StringToByteArrayV9 by Geograph
- StringToByteArrayV10 by AlejandroAlis
- StringToByteArrayV11 by Fredrik Hu
- StringToByteArrayV12 by Maarten Bodewes
- StringToByteArrayV13 by ClausAndersen
- StringToByteArrayV14 by Stas Makutin
- StringToByteArrayV15 by JJJ
- StringToByteArrayV16 by JamieSee
- StringToByteArrayV17 by spacepille
- StringToByteArrayV18 by Gregory Morse
- StringToByteArrayV19 by Rick
- StringToByteArrayV20 by SandRock
- StringToByteArrayV21 by Paul
Correctness Test
I have tested correctness by passing all 256 possible values of 1 byte, then checking output to see if correct. Result:
- V18 has issue with strings start with "00" (see Roger Stewart comment on it ). other than that it passes all tests.
- if hex string alphabet letters are uppercase: all functions successfully passed
- if hex string alphabet letters are lowercase then the following functions failed: V5_1, V5_2, v7, V8, V15, V19
note: V5_3 solves this issue (of V5_1 and V5_2)
Performance Test
I have done performance tests using Stopwatch class.
- Performance for long strings
input length: 10,000,000 bytes
runs: 100
average elapsed time per run:
V1 = 136.4ms
V2 = 104.5ms
V3 = 22.0ms
V4 = 9.9ms
V5_1 = 10.2ms
V5_2 = 9.0ms
V5_3 = 9.3ms
V6 = 18.3ms
V7 = 9.8ms
V8 = 8.8ms
V9 = 10.2ms
V10 = 19.0ms
V11 = 12.2ms
V12 = 27.4ms
V13 = 21.8ms
V14 = 12.0ms
V15 = 14.9ms
V16 = 15.3ms
V17 = 9.5ms
V18 got excluded from this test, because it was very slow when using very long string
V19 = 222.8ms
V20 = 66.0ms
V21 = 15.4ms
V1 average ticks per run: 1363529.4
V2 is more fast than V1 by: 1.3 times (ticks ratio)
V3 is more fast than V1 by: 6.2 times (ticks ratio)
V4 is more fast than V1 by: 13.8 times (ticks ratio)
V5_1 is more fast than V1 by: 13.3 times (ticks ratio)
V5_2 is more fast than V1 by: 15.2 times (ticks ratio)
V5_3 is more fast than V1 by: 14.8 times (ticks ratio)
V6 is more fast than V1 by: 7.4 times (ticks ratio)
V7 is more fast than V1 by: 13.9 times (ticks ratio)
V8 is more fast than V1 by: 15.4 times (ticks ratio)
V9 is more fast than V1 by: 13.4 times (ticks ratio)
V10 is more fast than V1 by: 7.2 times (ticks ratio)
V11 is more fast than V1 by: 11.1 times (ticks ratio)
V12 is more fast than V1 by: 5.0 times (ticks ratio)
V13 is more fast than V1 by: 6.3 times (ticks ratio)
V14 is more fast than V1 by: 11.4 times (ticks ratio)
V15 is more fast than V1 by: 9.2 times (ticks ratio)
V16 is more fast than V1 by: 8.9 times (ticks ratio)
V17 is more fast than V1 by: 14.4 times (ticks ratio)
V19 is more SLOW than V1 by: 1.6 times (ticks ratio)
V20 is more fast than V1 by: 2.1 times (ticks ratio)
V21 is more fast than V1 by: 8.9 times (ticks ratio)
- Performance of V18 for long strings
V18 took long time at the previous test,
so let's decrease length for it:
input length: 1,000,000 bytes
runs: 100
average elapsed time per run: V1 = 14.1ms , V18 = 146.7ms
V1 average ticks per run: 140630.3
V18 is more SLOW than V1 by: 10.4 times (ticks ratio)
- Performance for short strings
input length: 100 byte
runs: 1,000,000
V1 average ticks per run: 14.6
V2 is more fast than V1 by: 1.4 times (ticks ratio)
V3 is more fast than V1 by: 5.9 times (ticks ratio)
V4 is more fast than V1 by: 15.7 times (ticks ratio)
V5_1 is more fast than V1 by: 15.1 times (ticks ratio)
V5_2 is more fast than V1 by: 18.4 times (ticks ratio)
V5_3 is more fast than V1 by: 16.3 times (ticks ratio)
V6 is more fast than V1 by: 5.3 times (ticks ratio)
V7 is more fast than V1 by: 15.7 times (ticks ratio)
V8 is more fast than V1 by: 18.0 times (ticks ratio)
V9 is more fast than V1 by: 15.5 times (ticks ratio)
V10 is more fast than V1 by: 7.8 times (ticks ratio)
V11 is more fast than V1 by: 12.4 times (ticks ratio)
V12 is more fast than V1 by: 5.3 times (ticks ratio)
V13 is more fast than V1 by: 5.2 times (ticks ratio)
V14 is more fast than V1 by: 13.4 times (ticks ratio)
V15 is more fast than V1 by: 9.9 times (ticks ratio)
V16 is more fast than V1 by: 9.2 times (ticks ratio)
V17 is more fast than V1 by: 16.2 times (ticks ratio)
V18 is more fast than V1 by: 1.1 times (ticks ratio)
V19 is more SLOW than V1 by: 1.6 times (ticks ratio)
V20 is more fast than V1 by: 1.9 times (ticks ratio)
V21 is more fast than V1 by: 11.4 times (ticks ratio)
Testing Code
It is good idea to read Disclaimer section down here in this post, before using any from the following code https://github.com/Ghosticollis/performance-tests/blob/main/MTestPerformance.cs
Summary
I recommend using one of the following functions, because of the good performance, and support both upper and lower case:
- StringToByteArrayV4 by CoperNick
- StringToByteArrayV9 by Geograph
- StringToByteArrayV17 by spacepille
- StringToByteArrayV5_3 basically by Chris F (it is base on V5_1, but I have enhanced it based on Amir Rezaei's and Ben Voigt's comments).
Here is the final shape of V5_3:
static byte[] HexStringToByteArrayV5_3(string hexString) {
int hexStringLength = hexString.Length;
byte[] b = new byte[hexStringLength / 2];
for (int i = 0; i < hexStringLength; i += 2) {
int topChar = hexString[i];
topChar = (topChar > 0x40 ? (topChar & ~0x20) - 0x37 : topChar - 0x30) << 4;
int bottomChar = hexString[i + 1];
bottomChar = bottomChar > 0x40 ? (bottomChar & ~0x20) - 0x37 : bottomChar - 0x30;
b[i / 2] = (byte)(topChar + bottomChar);
}
return b;
}
Disclaimer
WARNING: I don't have proper knowledge in testing. The main purpose of these primitive tests is to give quick overview on what might be good from all of posted functions. If you need accurate results, please use proper testing tools.
Finally, I would like to say I am new to be active at stackoverflow, sorry if my post is lacking. comments to enhance this post would be appreciated.

- 26
- 1
- 8
And for inserting into an SQL string (if you're not using command parameters):
public static String ByteArrayToSQLHexString(byte[] Source)
{
return = "0x" + BitConverter.ToString(Source).Replace("-", "");
}

- 293
- 5
- 9
-
if `Source == null` or `Source.Length == 0` we have a problem sir! – Andrei Krasutski Jun 07 '19 at 17:37
This version of ByteArrayToHexViaByteManipulation could be faster.
From my reports:
- ByteArrayToHexViaByteManipulation3: 1,68 average ticks (over 1000 runs), 17,5X
- ByteArrayToHexViaByteManipulation2: 1,73 average ticks (over 1000 runs), 16,9X
- ByteArrayToHexViaByteManipulation: 2,90 average ticks (over 1000 runs), 10,1X
- ByteArrayToHexViaLookupAndShift: 3,22 average ticks (over 1000 runs), 9,1X
...
static private readonly char[] hexAlphabet = new char[] {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; static string ByteArrayToHexViaByteManipulation3(byte[] bytes) { 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] = hexAlphabet[b]; b = ((byte)(bytes[i] & 0xF)); c[i * 2 + 1] = hexAlphabet[b]; } return new string(c); }
And I think this one is an optimization:
static private readonly char[] hexAlphabet = new char[]
{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
static string ByteArrayToHexViaByteManipulation4(byte[] bytes)
{
char[] c = new char[bytes.Length * 2];
for (int i = 0, ptr = 0; i < bytes.Length; i++, ptr += 2)
{
byte b = bytes[i];
c[ptr] = hexAlphabet[b >> 4];
c[ptr + 1] = hexAlphabet[b & 0xF];
}
return new string(c);
}

- 86
- 3
I'll enter this bit fiddling competition as I have an answer that also uses bit-fiddling to decode hexadecimals. Note that using character arrays may be even faster as calling StringBuilder
methods will take time as well.
public static String ToHex (byte[] data)
{
int dataLength = data.Length;
// pre-create the stringbuilder using the length of the data * 2, precisely enough
StringBuilder sb = new StringBuilder (dataLength * 2);
for (int i = 0; i < dataLength; i++) {
int b = data [i];
// check using calculation over bits to see if first tuple is a letter
// isLetter is zero if it is a digit, 1 if it is a letter
int isLetter = (b >> 7) & ((b >> 6) | (b >> 5)) & 1;
// calculate the code using a multiplication to make up the difference between
// a digit character and an alphanumerical character
int code = '0' + ((b >> 4) & 0xF) + isLetter * ('A' - '9' - 1);
// now append the result, after casting the code point to a character
sb.Append ((Char)code);
// do the same with the lower (less significant) tuple
isLetter = (b >> 3) & ((b >> 2) | (b >> 1)) & 1;
code = '0' + (b & 0xF) + isLetter * ('A' - '9' - 1);
sb.Append ((Char)code);
}
return sb.ToString ();
}
public static byte[] FromHex (String hex)
{
// pre-create the array
int resultLength = hex.Length / 2;
byte[] result = new byte[resultLength];
// set validity = 0 (0 = valid, anything else is not valid)
int validity = 0;
int c, isLetter, value, validDigitStruct, validDigit, validLetterStruct, validLetter;
for (int i = 0, hexOffset = 0; i < resultLength; i++, hexOffset += 2) {
c = hex [hexOffset];
// check using calculation over bits to see if first char is a letter
// isLetter is zero if it is a digit, 1 if it is a letter (upper & lowercase)
isLetter = (c >> 6) & 1;
// calculate the tuple value using a multiplication to make up the difference between
// a digit character and an alphanumerical character
// minus 1 for the fact that the letters are not zero based
value = ((c & 0xF) + isLetter * (-1 + 10)) << 4;
// check validity of all the other bits
validity |= c >> 7; // changed to >>, maybe not OK, use UInt?
validDigitStruct = (c & 0x30) ^ 0x30;
validDigit = ((c & 0x8) >> 3) * (c & 0x6);
validity |= (isLetter ^ 1) * (validDigitStruct | validDigit);
validLetterStruct = c & 0x18;
validLetter = (((c - 1) & 0x4) >> 2) * ((c - 1) & 0x2);
validity |= isLetter * (validLetterStruct | validLetter);
// do the same with the lower (less significant) tuple
c = hex [hexOffset + 1];
isLetter = (c >> 6) & 1;
value ^= (c & 0xF) + isLetter * (-1 + 10);
result [i] = (byte)value;
// check validity of all the other bits
validity |= c >> 7; // changed to >>, maybe not OK, use UInt?
validDigitStruct = (c & 0x30) ^ 0x30;
validDigit = ((c & 0x8) >> 3) * (c & 0x6);
validity |= (isLetter ^ 1) * (validDigitStruct | validDigit);
validLetterStruct = c & 0x18;
validLetter = (((c - 1) & 0x4) >> 2) * ((c - 1) & 0x2);
validity |= isLetter * (validLetterStruct | validLetter);
}
if (validity != 0) {
throw new ArgumentException ("Hexadecimal encoding incorrect for input " + hex);
}
return result;
}
Converted from Java code.

- 90,524
- 13
- 150
- 263
-
1Hmm, I really should optimize this for `Char[]` and use `Char` internally instead of ints... – Maarten Bodewes Jan 20 '14 at 23:46
-
1For C#, initializing the variables where they are used, instead of outside the loop, is probably preferred to let the compiler optimize. I get equivalent performance either way. – Peteter Jun 12 '19 at 16:50
I did not get the code you suggested to work, Olipro. hex[i] + hex[i+1]
apparently returned an int
.
I did, however have some success by taking some hints from Waleeds code and hammering this together. It's ugly as hell but it seems to work and performs at 1/3 of the time compared to the others according to my tests (using patridges testing mechanism). Depending on input size. Switching around the ?:s to separate out 0-9 first would probably yield a slightly faster result since there are more numbers than letters.
public static byte[] StringToByteArray2(string hex)
{
byte[] bytes = new byte[hex.Length/2];
int bl = bytes.Length;
for (int i = 0; i < bl; ++i)
{
bytes[i] = (byte)((hex[2 * i] > 'F' ? hex[2 * i] - 0x57 : hex[2 * i] > '9' ? hex[2 * i] - 0x37 : hex[2 * i] - 0x30) << 4);
bytes[i] |= (byte)(hex[2 * i + 1] > 'F' ? hex[2 * i + 1] - 0x57 : hex[2 * i + 1] > '9' ? hex[2 * i + 1] - 0x37 : hex[2 * i + 1] - 0x30);
}
return bytes;
}

- 101,612
- 66
- 270
- 352

- 51
- 2
In terms of speed, this seems to be better than anything here:
public static string ToHexString(byte[] data) {
byte b;
int i, j, k;
int l = data.Length;
char[] r = new char[l * 2];
for (i = 0, j = 0; i < l; ++i) {
b = data[i];
k = b >> 4;
r[j++] = (char)(k > 9 ? k + 0x37 : k + 0x30);
k = b & 15;
r[j++] = (char)(k > 9 ? k + 0x37 : k + 0x30);
}
return new string(r);
}

- 21
- 1
For performance I would go with drphrozens solution. A tiny optimization for the decoder could be to use a table for either char to get rid of the "<< 4".
Clearly the two method calls are costly. If some kind of check is made either on input or output data (could be CRC, checksum or whatever) the if (b == 255)...
could be skipped and thereby also the method calls altogether.
Using offset++
and offset
instead of offset
and offset + 1
might give some theoretical benefit but I suspect the compiler handles this better than me.
private static readonly byte[] LookupTableLow = new byte[] {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
private static readonly byte[] LookupTableHigh = new byte[] {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
private static byte LookupLow(char c)
{
var b = LookupTableLow[c];
if (b == 255)
throw new IOException("Expected a hex character, got " + c);
return b;
}
private static byte LookupHigh(char c)
{
var b = LookupTableHigh[c];
if (b == 255)
throw new IOException("Expected a hex character, got " + c);
return b;
}
public static byte ToByte(char[] chars, int offset)
{
return (byte)(LookupHigh(chars[offset++]) | LookupLow(chars[offset]));
}
This is just off the top of my head and has not been tested or benchmarked.

- 30,738
- 21
- 105
- 131

- 19
- 1
Two mashups which folds the two nibble operations into one.
Probably pretty efficient version:
public static string ByteArrayToString2(byte[] ba)
{
char[] c = new char[ba.Length * 2];
for( int i = 0; i < ba.Length * 2; ++i)
{
byte b = (byte)((ba[i>>1] >> 4*((i&1)^1)) & 0xF);
c[i] = (char)(55 + b + (((b-10)>>31)&-7));
}
return new string( c );
}
Decadent linq-with-bit-hacking version:
public static string ByteArrayToString(byte[] ba)
{
return string.Concat( ba.SelectMany( b => new int[] { b >> 4, b & 0xF }).Select( b => (char)(55 + b + (((b-10)>>31)&-7))) );
}
And reverse:
public static byte[] HexStringToByteArray( string s )
{
byte[] ab = new byte[s.Length>>1];
for( int i = 0; i < s.Length; i++ )
{
int b = s[i];
b = (b - '0') + ((('9' - b)>>31)&-7);
ab[i>>1] |= (byte)(b << 4*((i&1)^1));
}
return ab;
}

- 509
- 1
- 6
- 14
Here's my shot at it. I've created a pair of extension classes to extend string and byte. On the large file test, the performance is comparable to Byte Manipulation 2.
The code below for ToHexString is an optimized implementation of the lookup and shift algorithm. It is almost identical to the one by Behrooz, but it turns out using a foreach
to iterate and a counter is faster than an explicitly indexing for
.
It comes in 2nd place behind Byte Manipulation 2 on my machine and is very readable code. The following test results are also of interest:
ToHexStringCharArrayWithCharArrayLookup: 41,589.69 average ticks (over 1000 runs), 1.5X ToHexStringCharArrayWithStringLookup: 50,764.06 average ticks (over 1000 runs), 1.2X ToHexStringStringBuilderWithCharArrayLookup: 62,812.87 average ticks (over 1000 runs), 1.0X
Based on the above results it seems safe to conclude that:
- The penalties for indexing into a string to perform the lookup vs. a char array are significant in the large file test.
- The penalties for using a StringBuilder of known capacity vs. a char array of known size to create the string are even more significant.
Here's the code:
using System;
namespace ConversionExtensions
{
public static class ByteArrayExtensions
{
private readonly static char[] digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
public static string ToHexString(this byte[] bytes)
{
char[] hex = new char[bytes.Length * 2];
int index = 0;
foreach (byte b in bytes)
{
hex[index++] = digits[b >> 4];
hex[index++] = digits[b & 0x0F];
}
return new string(hex);
}
}
}
using System;
using System.IO;
namespace ConversionExtensions
{
public static class StringExtensions
{
public static byte[] ToBytes(this string hexString)
{
if (!string.IsNullOrEmpty(hexString) && hexString.Length % 2 != 0)
{
throw new FormatException("Hexadecimal string must not be empty and must contain an even number of digits to be valid.");
}
hexString = hexString.ToUpperInvariant();
byte[] data = new byte[hexString.Length / 2];
for (int index = 0; index < hexString.Length; index += 2)
{
int highDigitValue = hexString[index] <= '9' ? hexString[index] - '0' : hexString[index] - 'A' + 10;
int lowDigitValue = hexString[index + 1] <= '9' ? hexString[index + 1] - '0' : hexString[index + 1] - 'A' + 10;
if (highDigitValue < 0 || lowDigitValue < 0 || highDigitValue > 15 || lowDigitValue > 15)
{
throw new FormatException("An invalid digit was encountered. Valid hexadecimal digits are 0-9 and A-F.");
}
else
{
byte value = (byte)((highDigitValue << 4) | (lowDigitValue & 0x0F));
data[index / 2] = value;
}
}
return data;
}
}
}
Below are the test results that I got when I put my code in @patridge's testing project on my machine. I also added a test for converting to a byte array from hexadecimal. The test runs that exercised my code are ByteArrayToHexViaOptimizedLookupAndShift and HexToByteArrayViaByteManipulation. The HexToByteArrayViaConvertToByte was taken from XXXX. The HexToByteArrayViaSoapHexBinary is the one from @Mykroft's answer.
Intel Pentium III Xeon processor
Cores: 4 <br/> Current Clock Speed: 1576 <br/> Max Clock Speed: 3092 <br/>
Converting array of bytes into hexadecimal string representation
ByteArrayToHexViaByteManipulation2: 39,366.64 average ticks (over 1000 runs), 22.4X
ByteArrayToHexViaOptimizedLookupAndShift: 41,588.64 average ticks (over 1000 runs), 21.2X
ByteArrayToHexViaLookup: 55,509.56 average ticks (over 1000 runs), 15.9X
ByteArrayToHexViaByteManipulation: 65,349.12 average ticks (over 1000 runs), 13.5X
ByteArrayToHexViaLookupAndShift: 86,926.87 average ticks (over 1000 runs), 10.2X
ByteArrayToHexStringViaBitConverter: 139,353.73 average ticks (over 1000 runs),6.3X
ByteArrayToHexViaSoapHexBinary: 314,598.77 average ticks (over 1000 runs), 2.8X
ByteArrayToHexStringViaStringBuilderForEachByteToString: 344,264.63 average ticks (over 1000 runs), 2.6X
ByteArrayToHexStringViaStringBuilderAggregateByteToString: 382,623.44 average ticks (over 1000 runs), 2.3X
ByteArrayToHexStringViaStringBuilderForEachAppendFormat: 818,111.95 average ticks (over 1000 runs), 1.1X
ByteArrayToHexStringViaStringConcatArrayConvertAll: 839,244.84 average ticks (over 1000 runs), 1.1X
ByteArrayToHexStringViaStringBuilderAggregateAppendFormat: 867,303.98 average ticks (over 1000 runs), 1.0X
ByteArrayToHexStringViaStringJoinArrayConvertAll: 882,710.28 average ticks (over 1000 runs), 1.0X

- 30,738
- 21
- 105
- 131

- 12,696
- 2
- 31
- 47
Not optimized for speed, but more LINQy than most answers (.NET 4.0):
<Extension()>
Public Function FromHexToByteArray(hex As String) As Byte()
hex = If(hex, String.Empty)
If hex.Length Mod 2 = 1 Then hex = "0" & hex
Return Enumerable.Range(0, hex.Length \ 2).Select(Function(i) Convert.ToByte(hex.Substring(i * 2, 2), 16)).ToArray
End Function
<Extension()>
Public Function ToHexString(bytes As IEnumerable(Of Byte)) As String
Return String.Concat(bytes.Select(Function(b) b.ToString("X2")))
End Function

- 2,897
- 2
- 38
- 54
Another fast function...
private static readonly byte[] HexNibble = new byte[] {
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7,
0x8, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF
};
public static byte[] HexStringToByteArray( string str )
{
int byteCount = str.Length >> 1;
byte[] result = new byte[byteCount + (str.Length & 1)];
for( int i = 0; i < byteCount; i++ )
result[i] = (byte) (HexNibble[str[i << 1] - 48] << 4 | HexNibble[str[(i << 1) + 1] - 48]);
if( (str.Length & 1) != 0 )
result[byteCount] = (byte) HexNibble[str[str.Length - 1] - 48];
return result;
}

- 30,738
- 21
- 105
- 131

- 4,071
- 2
- 15
- 5
Another way is by using stackalloc
to reduce GC memory pressure:
static string ByteToHexBitFiddle(byte[] bytes)
{
var c = stackalloc char[bytes.Length * 2 + 1];
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));
}
c[bytes.Length * 2 ] = '\0';
return new string(c);
}

- 1,217
- 11
- 21
Shortest way and .net core supported:
public static string BytesToString(byte[] ba) =>
ba.Aggregate(new StringBuilder(32), (sb, b) => sb.Append(b.ToString("X2"))).ToString();

- 4,950
- 4
- 49
- 69
-
1This is great, as is, if `ba` is 16 bytes or less. But, what it should be is `new StringBuilder(ba.Length * 2)` so it efficiently handles any length byte array. – Andy Mar 09 '21 at 02:50
There is a simple one-liner solution not yet mentioned that will convert hex strings into byte arrays (we don't care about negative interpretation here as it does not matter):
BigInteger.Parse(str, System.Globalization.NumberStyles.HexNumber).ToByteArray().Reverse().ToArray();

- 369
- 2
- 10
-
This does not preserve leading bytes of 0. For instance, a string of `"000080"` results in a one byte array of `{ 0x80 }` and not the expected three byte array of `{ 0x00, 0x00, 0x80 }` – Roger Stewart Mar 11 '20 at 12:52
-
Yes I suppose something along the lines of Enumerable.Repeat
(0, (len(str) / 2 - len(bigIntBytes)).Concat(bigIntBytes).ToArray() is needed in that case – Gregory Morse Mar 12 '20 at 13:33 -
Instead of reversing the array, you can make the array in big endian mode: `.ToByteArray(isBigEndian: true)` – Simon Bondo Apr 17 '20 at 14:28
-
Thanks Simon, I did not know or at least forgot about this convenient parameter! – Gregory Morse Apr 18 '20 at 16:43
Yet another variation for diversity:
public static byte[] FromHexString(string src)
{
if (String.IsNullOrEmpty(src))
return null;
int index = src.Length;
int sz = index / 2;
if (sz <= 0)
return null;
byte[] rc = new byte[sz];
while (--sz >= 0)
{
char lo = src[--index];
char hi = src[--index];
rc[sz] = (byte)(
(
(hi >= '0' && hi <= '9') ? hi - '0' :
(hi >= 'a' && hi <= 'f') ? hi - 'a' + 10 :
(hi >= 'A' && hi <= 'F') ? hi - 'A' + 10 :
0
)
<< 4 |
(
(lo >= '0' && lo <= '9') ? lo - '0' :
(lo >= 'a' && lo <= 'f') ? lo - 'a' + 10 :
(lo >= 'A' && lo <= 'F') ? lo - 'A' + 10 :
0
)
);
}
return rc;
}

- 219
- 2
- 4
This works to go from string to byte array...
public static byte[] StrToByteArray(string str)
{
Dictionary<string, byte> hexindex = new Dictionary<string, byte>();
for (byte i = 0; i < 255; i++)
hexindex.Add(i.ToString("X2"), i);
List<byte> hexres = new List<byte>();
for (int i = 0; i < str.Length; i += 2)
hexres.Add(hexindex[str.Substring(i, 2)]);
return hexres.ToArray();
}

- 139
- 1
- 2
I guess its speed is worth 16 extra bytes.
static char[] hexes = new char[]{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
public static string ToHexadecimal (this byte[] Bytes)
{
char[] Result = new char[Bytes.Length << 1];
int Offset = 0;
for (int i = 0; i != Bytes.Length; i++) {
Result[Offset++] = hexes[Bytes[i] >> 4];
Result[Offset++] = hexes[Bytes[i] & 0x0F];
}
return new string(Result);
}

- 1,696
- 2
- 32
- 54
-
2It's actually slower than other table lookup based approaches(at least in my tests). Using `!=` instead of `<` breaks some JIT optimization patters, and the extra counter for `Offset` seems costly as well. – CodesInChaos Jan 15 '13 at 09:32
There is also XmlWriter.WriteBinHex
(see the MSDN page). This is very useful if you need to put the hexadecimal string into an XML stream.
Here is a standalone method to see how it works:
public static string ToBinHex(byte[] bytes)
{
XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
xmlWriterSettings.ConformanceLevel = ConformanceLevel.Fragment;
xmlWriterSettings.CheckCharacters = false;
xmlWriterSettings.Encoding = ASCIIEncoding.ASCII;
MemoryStream memoryStream = new MemoryStream();
using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings))
{
xmlWriter.WriteBinHex(bytes, 0, bytes.Length);
}
return Encoding.ASCII.GetString(memoryStream.ToArray());
}
The following expands the excellent answer here by allowing native lower case option as well, and also handles null or empty input and makes this an extension method.
/// <summary>
/// Converts the byte array to a hex string very fast. Excellent job
/// with code lightly adapted from 'community wiki' here: https://stackoverflow.com/a/14333437/264031
/// (the function was originally named: ByteToHexBitFiddle). Now allows a native lowerCase option
/// to be input and allows null or empty inputs (null returns null, empty returns empty).
/// </summary>
public static string ToHexString(this byte[] bytes, bool lowerCase = false)
{
if (bytes == null)
return null;
else if (bytes.Length == 0)
return "";
char[] c = new char[bytes.Length * 2];
int b;
int xAddToAlpha = lowerCase ? 87 : 55;
int xAddToDigit = lowerCase ? -39 : -7;
for (int i = 0; i < bytes.Length; i++) {
b = bytes[i] >> 4;
c[i * 2] = (char)(xAddToAlpha + b + (((b - 10) >> 31) & xAddToDigit));
b = bytes[i] & 0xF;
c[i * 2 + 1] = (char)(xAddToAlpha + b + (((b - 10) >> 31) & xAddToDigit));
}
string val = new string(c);
return val;
}
public static string ToHexString(this IEnumerable<byte> bytes, bool lowerCase = false)
{
if (bytes == null)
return null;
byte[] arr = bytes.ToArray();
return arr.ToHexString(lowerCase);
}

- 1
- 1

- 9,104
- 7
- 59
- 69
static string ByteArrayToHexViaLookupPerByte2(byte[] bytes)
{
var result3 = new uint[bytes.Length];
for (int i = 0; i < bytes.Length; i++)
result3[i] = _Lookup32[bytes[i]];
var handle = GCHandle.Alloc(result3, GCHandleType.Pinned);
try
{
var result = Marshal.PtrToStringUni(handle.AddrOfPinnedObject(), bytes.Length * 2);
return result;
}
finally
{
handle.Free();
}
}
This functions in my tests is always the second entry after the unsafe implementation.
Unfortunately, the test bench is not so reliable... if you run it multiple times the list got shuffled so much that who knows after the unsafe which is really the fastest! It doesn't take into a account pre-warming, jit compilation time, and GC performance hits. I would like to have rewritten it to have more information, but I didn't had really the time for it.

- 443
- 5
- 7
If performance matters, here's an optimized solution:
static readonly char[] _hexDigits = "0123456789abcdef".ToCharArray();
public static string ToHexString(this byte[] bytes)
{
char[] digits = new char[bytes.Length * 2];
for (int i = 0; i < bytes.Length; i++)
{
int d1, d2;
d1 = Math.DivRem(bytes[i], 16, out d2);
digits[2 * i] = _hexDigits[d1];
digits[2 * i + 1] = _hexDigits[d2];
}
return new string(digits);
}
It's about 2.5 times faster that BitConverter.ToString
, and about 7 times faster that BitConverter.ToString
+ removal of the '-' chars.

- 286,951
- 70
- 623
- 758
-
4If performance mattered, you would not use `Math.DivRem` to split a byte into two nibbles. – dolmen Aug 20 '13 at 23:53
-
@dolmen, did you run performance tests with and without `Math.DivRem`? I seriously doubt it has *any* effect on performance: The implementation of `Math.DivRem` is exactly what you would do manually, and the method is very simple so it's always inlined by the JIT (actually it's intended to be inlined, as suggested by the `TargetedPatchingOptOut` attribute applied to it) – Thomas Levesque Aug 21 '13 at 00:12
-
1@ThomasLevesque The implementation of DivRem performs a modulus operation and a division. Why do you assume that those operations are exactly what you would do manually? For me, the natural implementation is https://github.com/patridge/PerformanceStubs/blob/master/PerformanceStubs/Tests/ConvertByteArrayToHexString/Test.cs#L101 which performs a bitshift and a logical and. Those operations are far cheaper than modulus and division even on modern processors. – Søren Boisen Aug 03 '16 at 16:08
I came up with a different code that is tolerant to extra characters (whitespace, dash...). It is mostly inspired from some acceptably-fast answers here. It allows parsing of the following "file"
00-aa-84-fb
12 32 FF CD
12 00
12_32_FF_CD
1200d5e68a
/// <summary>Reads a hex string into bytes</summary>
public static IEnumerable<byte> HexadecimalStringToBytes(string hex) {
if (hex == null)
throw new ArgumentNullException(nameof(hex));
char c, c1 = default(char);
bool hasc1 = false;
unchecked {
for (int i = 0; i < hex.Length; i++) {
c = hex[i];
bool isValid = 'A' <= c && c <= 'f' || 'a' <= c && c <= 'f' || '0' <= c && c <= '9';
if (!hasc1) {
if (isValid) {
hasc1 = true;
}
} else {
hasc1 = false;
if (isValid) {
yield return (byte)((GetHexVal(c1) << 4) + GetHexVal(c));
}
}
c1 = c;
}
}
}
/// <summary>Reads a hex string into a byte array</summary>
public static byte[] HexadecimalStringToByteArray(string hex)
{
if (hex == null)
throw new ArgumentNullException(nameof(hex));
var bytes = new List<byte>(hex.Length / 2);
foreach (var item in HexadecimalStringToBytes(hex)) {
bytes.Add(item);
}
return bytes.ToArray();
}
private static byte GetHexVal(char val)
{
return (byte)(val - (val < 0x3A ? 0x30 : val < 0x5B ? 0x37 : 0x57));
// ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^
// digits 0-9 upper char A-Z a-z
}
Please refer to full code when copying. Unit tests included.
Some might say it is too much tolerant to extra chars. So don't rely on this code to perform validation (or change it).

- 5,276
- 3
- 30
- 49
// a safe version of the lookup solution:
public static string ByteArrayToHexViaLookup32Safe(byte[] bytes, bool withZeroX)
{
if (bytes.Length == 0)
{
return withZeroX ? "0x" : "";
}
int length = bytes.Length * 2 + (withZeroX ? 2 : 0);
StateSmall stateToPass = new StateSmall(bytes, withZeroX);
return string.Create(length, stateToPass, (chars, state) =>
{
int offset0x = 0;
if (state.WithZeroX)
{
chars[0] = '0';
chars[1] = 'x';
offset0x += 2;
}
Span<uint> charsAsInts = MemoryMarshal.Cast<char, uint>(chars.Slice(offset0x));
int targetLength = state.Bytes.Length;
for (int i = 0; i < targetLength; i += 1)
{
uint val = Lookup32[state.Bytes[i]];
charsAsInts[i] = val;
}
});
}
private struct StateSmall
{
public StateSmall(byte[] bytes, bool withZeroX)
{
Bytes = bytes;
WithZeroX = withZeroX;
}
public byte[] Bytes;
public bool WithZeroX;
}

- 707
- 5
- 9
Combined a few answers into a class for my later copy and paste convenience:
/// <summary>
/// Extension methods to quickly convert byte array to string and back.
/// </summary>
public static class HexConverter
{
/// <summary>
/// Map values to hex digits
/// </summary>
private static readonly char[] HexDigits =
{
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
/// <summary>
/// Map 56 characters between ['0', 'F'] to their hex equivalents, and set invalid characters
/// such that they will overflow byte to fail conversion.
/// </summary>
private static readonly ushort[] HexValues =
{
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100,
0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100,
0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x000A, 0x000B,
0x000C, 0x000D, 0x000E, 0x000F
};
/// <summary>
/// Empty byte array
/// </summary>
private static readonly byte[] Empty = new byte[0];
/// <summary>
/// Convert a byte array to a hexadecimal string.
/// </summary>
/// <param name="bytes">
/// The input byte array.
/// </param>
/// <returns>
/// A string of hexadecimal digits.
/// </returns>
public static string ToHexString(this byte[] bytes)
{
var c = new char[bytes.Length * 2];
for (int i = 0, j = 0; i < bytes.Length; i++)
{
c[j++] = HexDigits[bytes[i] >> 4];
c[j++] = HexDigits[bytes[i] & 0x0F];
}
return new string(c);
}
/// <summary>
/// Parse a string of hexadecimal digits into a byte array.
/// </summary>
/// <param name="hexadecimalString">
/// The hexadecimal string.
/// </param>
/// <returns>
/// The parsed <see cref="byte[]"/> array.
/// </returns>
/// <exception cref="ArgumentException">
/// The input string either contained invalid characters, or was of an odd length.
/// </exception>
public static byte[] ToByteArray(string hexadecimalString)
{
if (!TryParse(hexadecimalString, out var value))
{
throw new ArgumentException("Invalid hexadecimal string", nameof(hexadecimalString));
}
return value;
}
/// <summary>
/// Parse a hexadecimal string to bytes
/// </summary>
/// <param name="hexadecimalString">
/// The hexadecimal string, which must be an even number of characters.
/// </param>
/// <param name="value">
/// The parsed value if successful.
/// </param>
/// <returns>
/// True if successful.
/// </returns>
public static bool TryParse(string hexadecimalString, out byte[] value)
{
if (hexadecimalString.Length == 0)
{
value = Empty;
return true;
}
if (hexadecimalString.Length % 2 != 0)
{
value = Empty;
return false;
}
try
{
value = new byte[hexadecimalString.Length / 2];
for (int i = 0, j = 0; j < hexadecimalString.Length; i++)
{
value[i] = (byte)((HexValues[hexadecimalString[j++] - '0'] << 4)
| HexValues[hexadecimalString[j++] - '0']);
}
return true;
}
catch (OverflowException)
{
value = Empty;
return false;
}
}
}

- 1,011
- 10
- 13
If you want to get the "4x speed increase" reported by wcoenen, then if it's not obvious: replace hex.Substring(i, 2)
with hex[i]+hex[i+1]
You could also take it a step further and get rid of the i+=2
by using i++
in both places.

- 30,738
- 21
- 105
- 131

- 3,489
- 19
- 25
Basic Solution With Extension Support
public static class Utils
{
public static byte[] ToBin(this string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
public static string ToHex(this byte[] ba)
{
return BitConverter.ToString(ba).Replace("-", "");
}
}
And use this class like below
byte[] arr1 = new byte[] { 1, 2, 3 };
string hex1 = arr1.ToHex();
byte[] arr2 = hex1.ToBin();

- 4,829
- 1
- 30
- 25
Here's my purely binary solution without a need for a library lookup, and also supports upper/lower case:
public static String encode(byte[] bytes, boolean uppercase) {
char[] result = new char[2 * bytes.length];
for (int i = 0; i < bytes.length; i++) {
byte word = bytes[i];
byte left = (byte) ((0XF0 & word) >>> 4);
byte right = (byte) ((byte) 0X0F & word);
int resultIndex = i * 2;
result[resultIndex] = encode(left, uppercase);
result[resultIndex + 1] = encode(right, uppercase);
}
return new String(result);
}
public static char encode(byte value, boolean uppercase) {
int characterCase = uppercase ? 0 : 32;
if (value > 15 || value < 0) {
return '0';
}
if (value > 9) {
return (char) (value + 0x37 | characterCase);
}
return (char) (value + 0x30);
}

- 3,989
- 9
- 48
- 84
Expanding on the BigInteger approach (Gregory Morse mentioned it above). I can't comment on efficiency and it uses System.Linq.Reverse(), but its small and built in.
// To hex
byte[] bytes = System.Text.Encoding.UTF8.GetBytes("Test String!£");
string hexString = new System.Numerics.BigInteger(bytes.Reverse().ToArray()).ToString("x2");
// From hex
byte[] fromHexBytes = System.Numerics.BigInteger.Parse(hexString, System.Globalization.NumberStyles.HexNumber).ToByteArray().Reverse().ToArray();
// Unit test
CollectionAssert.AreEqual(bytes, fromHexBytes);

- 1,023
- 8
- 15
With Java 8 , we ca use Byte.toUnsignedInt
public static String convertBytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte byt : bytes) {
int decimal = Byte.toUnsignedInt(byt);
String hex = Integer.toHexString(decimal);
result.append(hex);
}
return result.toString();
}

- 8,878
- 2
- 43
- 52
-
1Incorrect, `toHexString` may return just one character instead of two. – Maarten Bodewes May 02 '20 at 00:35
I suspect the speed of this will knock the socks off most of the other tests...
Public Function BufToHex(ByVal buf() As Byte) As String
Dim sB As New System.Text.StringBuilder
For i As Integer = 0 To buf.Length - 1
sB.Append(buf(i).ToString("x2"))
Next i
Return sB.ToString
End Function
-
3What makes you think that? You create a new string object for every byte in the buffer, and you don't pre-size the string builder (which can lead to the buffer being resized multiple times on large arrays). – Brian Reichle Dec 02 '11 at 13:50
-