C# - byte 배열을 Hex(16진수) 문자열로 고속 변환하는 방법
재미있는 답변이 있군요. ^^
How do you convert a byte array to a hexadecimal string, and vice versa?
; https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa
위의 글에 보면 byte 배열의 값을 각각
16진수 문자열로 변환하는 다양한 방법에 대해 성능을 비교한 덧글을 볼 수 있습니다. 그중에서 가장 빠른 방법이 "Lookup by byte unsafe (via CodesInChaos)"라고 소개하는데요,
How do you convert a byte array to a hexadecimal string, and vice versa?
- Lookup by byte unsafe (via CodesInChaos)
; https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa/24343727#24343727
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);
}
동적 프로그래밍을 할 때도 마찬가지고, 언제나 성능은 cache가 정답으로 보입니다. ^^ (혹시 저 소스 코드보다 더 빠르게 최적화하신 분이 계실까요? ^^)
실제로 비교를 한 번 해보겠습니다. 우선, 코드가 간단해서 우리가 흔히 쓰는
BitConverter를 이용한 방법과,
// BitConverter 버전
BitConverter.ToString(buf).Replace("-", "");
아무래도 저건 루프를 두 번 돌 테니 직접 만들어서 구현한 코드를 놓고,
// ToHex 버전
StringBuilder sb = new StringBuilder(buf.Length * 2);
foreach (byte b in buf)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
함께 비교해 보면 다음과 같은 성능 수치를 확인할 수 있습니다.
// x64 + Release 빌드, 8192 바이트에 대해 10,000 회 테스트
BitConverter : 1153
ToHex : 4738
UnsafeLookup : 91
오호... 의외군요, StringBuilder를 이용해 루프를 한 번 돌도록 만든 "ToHex" 버전보다 BitConverter가 더 빠릅니다. 물론, UnsafeLookup은 압도적으로 빠르고. ^^
그런데, ToHex 버전을 StringBuilder를 사용하지 않고 BitConverter의 내부 코드를 조금 인용해 다음과 같이 만들어 볼 수도 있습니다.
char[] text = new char[buf.Length * 2];
int srcPos = 0;
for (int dstPos = 0; dstPos < text.Length; dstPos += 2)
{
byte b = buf[srcPos++];
text[dstPos] = GetHexValue(((int)b) / 16);
text[dstPos + 1] = GetHexValue(((int)b) % 16);
}
return new string(text);
static char GetHexValue(int number)
{
if (number < 10)
{
return (char)(number + 48);
}
return (char)(number - 10 + 65);
}
그럼 BitConverter보다 성능이 (당연히) 더 좋습니다.
BitConverter : 1164
ToHex : 240
UnsafeLookup : 96
(그러니까,
괜히 코드를 어설프게 만들면 마이크로소프트 측에서 만든 BitConverter보다 못한 성능을 내는 것입니다. ^^)
아래는 이 글에서 테스트한 전체 소스 코드입니다. (
첨부 파일로 프로젝트를 올려 두었습니다.)
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
class Program
{
static void Main(string[] args)
{
Action<int, string, Action<int, byte[]>, byte[]> action = (loopCount, title, work, arg) =>
{
Stopwatch st = new Stopwatch();
st.Start();
work(loopCount, arg);
st.Stop();
Console.WriteLine(title + " : " + st.ElapsedMilliseconds);
};
action(1, "BitConverter", UseBitConverter, new byte[] { 0 });
action(1, "ToHex", ToHex, new byte[] { 0 });
action(1, "UnsafeLookup", UnsafeLookup, new byte[] { 0 });
Console.WriteLine();
action(10000, "BitConverter", UseBitConverter, new byte[8192]);
action(10000, "ToHex", ToHex, new byte[8192]);
action(10000, "UnsafeLookup", UnsafeLookup, new byte[8192]);
}
private static void UseBitConverter(int loopCount, byte[] buf)
{
for (int i = 0; i < loopCount; i++)
{
BitConverter.ToString(buf).Replace("-", "");
}
}
static string ConvertWithStringBuilder(byte[] buf)
{
StringBuilder sb = new StringBuilder(buf.Length * 2);
foreach (byte b in buf)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
static string ConvertToHex(byte[] buf)
{
char[] text = new char[buf.Length * 2];
int srcPos = 0;
for (int dstPos = 0; dstPos < text.Length; dstPos += 2)
{
byte b = buf[srcPos++];
text[dstPos] = GetHexValue(((int)b) / 16);
text[dstPos + 1] = GetHexValue(((int)b) % 16);
}
return new string(text);
}
static char GetHexValue(int number)
{
if (number < 10)
{
return (char)(number + 48);
}
return (char)(number - 10 + 65);
}
private static void ToHex(int loopCount, byte [] buf)
{
for (int i = 0; i < loopCount; i ++)
{
ConvertToHex(buf);
// ConvertWithStringBuilder(buf);
}
}
private static void UnsafeLookup(int loopCount, byte[] buf)
{
for (int i = 0; i < loopCount; i++)
{
ByteToHex.ByteArrayToHexViaLookup32Unsafe(buf);
}
}
}
public unsafe class ByteToHex
{
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);
}
}
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]