[C#] 바이트 배열을 16 진수 문자열로 변환하는 방법은 무엇입니까?


Answers

성능 분석

참고 : 2015-08-20 현재의 새로운 지도자.

Stopwatch 성능 테스트, 무작위 문장 (n = 61, 1000 반복) 및 Project Gutenburg 텍스트 (n = 1,238,957, 150 반복)를 사용한 실행을 통해 다양한 변환 방법을 각각 실행했습니다. 대략적으로 가장 빠른 것부터 가장 느린 것까지 결과가 있습니다. 모든 측정 값은 틱 단위 ( 10,000 tick = 1ms )이며 모든 관련 노트는 [가장 느린] StringBuilder 구현과 비교됩니다. 사용 된 코드에 대해서는 아래를 참조하거나 테스트 프레임 워크 레포 에서이 코드를 실행해야합니다.

부인 성명

경고 : 콘크리트에 대해서는이 통계에 의존하지 마십시오. 그들은 단순히 샘플 데이터의 샘플 실행입니다. 최고 수준의 성능이 정말로 필요한 경우에는 제작 방법을 나타내는 환경에서 이러한 방법을 테스트하고 사용하려는 데이터를 나타내는 데이터를 사용하십시오.

결과

  • CodesInChaos를 통해 unsafe 바이트로 검색 (airbreather의 테스트 레포에 airbreather )
    • 텍스트 : 4,727.85 (105.2X)
    • 문장 : 0.28 (99.7X)
  • 바이트 별 조회 (CodesInChaos를 통해)
    • 텍스트 : 10,853.96 (45.8X 빨라짐)
    • 문장 : 0.65 (42.7X 빨라짐)
  • 바이트 조작 2 (CodesInChaos 경유)
    • 텍스트 : 12,967.69 (38.4X 빨라짐)
    • 문장 : 0.73 (37.9X 빨라짐)
  • 바이트 조작 (Waleed Eissa를 통해)
    • 텍스트 : 16,856.64 (29.5X 빨라짐)
    • 문장 : 0.70 (39.5X 빨라짐)
  • 조회 / 시프트 (Nathan Moinvaziri를 통해)
    • 텍스트 : 23,201.23 (21.4X 빨라짐)
    • 문장 : 1.24 (22.3X 빨라짐)
  • 니블 조회 (브라이언 램버트 경유)
    • 텍스트 : 23,879.41 (20.8X 빨라짐)
    • 문장 : 1.15 (23.9X 빨라짐)
  • BitConverter (Tomalak을 통해)
    • 텍스트 : 113,269.34 (4.4X 빨라짐)
    • 문장 : 9.98 (2.8X 빨라짐)
  • {SoapHexBinary}.ToString (Mykroft를 통해)
    • 텍스트 : 178,601.39 (2.8 배 빨라짐)
    • 문장 : 10.68 (2.6X 빨라짐)
  • {byte}.ToString("X2") ( foreach 사용 {byte}.ToString("X2") ( {byte}.ToString("X2") Dean의 답변에서 파생 됨)
    • 텍스트 : 308,805.38 (2.4X 빨라짐)
    • 문장 : 16.89 (2.4X 빠름)
  • {byte}.ToString("X2") ( {IEnumerable}.Aggregate , System.Linq 필요) (Mark를 통해)
    • 텍스트 : 352,828.20 (2.1 배 빨라짐)
    • 문장 : 16.87 (2.4X 빠름)
  • Array.ConvertAll ( Array.ConvertAll 사용) (Will Dean을 통해)
    • 텍스트 : 675,451.57 (1.1 배 빨라짐)
    • 문장 : 17.95 (2.2X 빨라짐)
  • Array.ConvertAll ( string.Concat 사용, .NET 4.0 필요) (Will Dean을 통해)
    • 텍스트 : 752,078.70 (1.0X 빨라짐)
    • 문장 : 18.28 (2.2X 빨라짐)
  • {StringBuilder}.AppendFormat ( foreach 사용) (Tomalak을 통해)
    • 텍스트 : 672,115.77 (1.1X 빨라짐)
    • 문장 : 36.82 (1.1 배 빨라짐)
  • {StringBuilder}.AppendFormat ( {IEnumerable}.Aggregate {StringBuilder}.AppendFormat 사용, System.Linq 필요) (Tomalak의 답변에서 파생 됨)
    • 텍스트 : 718,380.63 (1.0X 빨라짐)
    • 문장 : 39.71 (1.0X 빨라짐)

조회 테이블은 바이트 조작보다 앞서갔습니다. 기본적으로 어떤 주어진 니블이나 바이트가 16 진수 일지를 미리 계산하는 형식이 있습니다. 그런 다음 데이터를 훑어 보면 다음 부분을 조회하여 16 진수 문자열이 무엇인지 확인할 수 있습니다. 그런 다음 그 값은 어떤 방식으로 결과 문자열 출력에 추가됩니다. 오랫동안 바이트 조작을 위해 일부 개발자는 읽을 수 없도록하는 것이 가장 성과가 좋은 방법이었습니다.

최선의 방법은 여전히 ​​일부 대표 데이터를 찾아 생산 환경과 같은 방식으로 시도하는 것입니다. 다른 메모리 제약 조건이있는 경우 할당 속도가 더 빠르지 만 더 많은 메모리를 소비하는 할당보다 적은 방법을 선호 할 수 있습니다.

테스트 코드

내가 사용한 테스트 코드로 자유롭게 플레이하십시오. 여기에 버전이 포함되어 있지만 repo 를 복제하고 직접 방법을 추가하십시오. 흥미로운 것을 찾거나 사용하는 테스트 프레임 워크를 개선하려는 경우 pull 요청을 제출하십시오.

  1. /Tests/ConvertByteArrayToHexString/Test.cs에 새 정적 메서드 ( Func<byte[], string> )를 추가합니다.
  2. 해당 클래스의 TestCandidates 반환 값에 해당 메서드의 이름을 추가합니다.
  3. 동일한 클래스의 GenerateTestInput 에있는 주석을 토글하여 원하는 입력 버전 (문장 또는 텍스트)을 실행하는지 확인하십시오.
  4. F5 키를 누르고 출력을 기다립니다. HTML 덤프는 / bin 폴더에서도 생성됩니다.
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();
}

업데이트 (2010-01-13)

분석에 대한 Waleed의 대답을 추가했습니다. 아주 빠릅니다.

업데이트 (2011-10-05)

완성을 위해 string.Concat Array.ConvertAll 변형을 추가했습니다 (.NET 4.0 필요). string.Joinstring.Join 합니다. string.Join 버전.

업데이트 (2012-02-05)

테스트 레포에는 StringBuilder.Append(b.ToString("X2")) 와 같은 다양한 변형이 포함됩니다. 결과를 전혀 뒤엎 지 않습니다. foreach{IEnumerable}.Aggregate BitConverter 보다 빠르지 만 BitConverter 여전히 승리합니다.

업데이트 (2012-04-03)

3 위를 차지한 Mykroft의 SoapHexBinary 대답을 분석에 추가했습니다.

업데이트 (2013-01-15)

CodesInChaos의 바이트 조작 대답을 추가하여 첫 번째 자리를 차지했습니다 (큰 텍스트 블록에 큰 차이가 있음).

업데이트 (2013-05-23)

Brian Lambert의 블로그에서 Nathan Moinvaziri의 조회 답변과 변형을 추가했습니다. 둘 다 오히려 빠르지 만 필자가 사용했던 테스트 머신에서 선두 자리를 지키지 않았습니다 (AMD Phenom 9750).

업데이트 (2014-07-31)

@ CodesInChaos의 새로운 바이트 기반 조회 대답을 추가했습니다. 문장 테스트와 전문 텍스트 테스트 모두에서 주도권을 잡은 것으로 보인다.

업데이트 (2015-08-20)

답변의 repo에 airbreather 최적화 및 unsafe 변형이 추가되었습니다. 안전하지 않은 게임에서 게임을하고 싶다면 짧은 문자열과 큰 텍스트 모두에서 이전 우승자보다 큰 성능 향상을 얻을 수 있습니다.

Question

어떻게 바이트 배열을 16 진수 문자열로 변환 할 수 있습니까?




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



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



이것은 share (및 후속 수정 사항) share 개정판 4 에 share 입니다.

이 편집이 잘못되었다는 사실을 알리고 왜 되돌릴 수 있는지 설명합니다. 길을 따라 몇 가지 내부 구조에 대해 한 두 가지를 배울 수 있으며 조기 최적화가 실제로 무엇인지, 그리고 어떻게 조만간 당신을 물릴 수 있는지에 대한 또 다른 예를 볼 수 있습니다.

tl; dr : Convert.ToByteString.Substring 사용하면 바쁘다 (아래 "원본 코드"). Convert.ToByte 를 다시 구현하지 않으려는 경우 가장 좋은 조합입니다. 성능이 필요한 경우 Convert.ToByte 사용하지 않는 고급 기능을 사용하십시오 (다른 답변 참조). Convert.ToByte 와 함께 String.Substring 이외의 다른 것을 사용하지 마십시오. String.Substring 답변에 대한 의견에서이 점에 대해 흥미로운 점이 없으면 말입니다.

경고 : Convert.ToByte(char[], Int32) 오버로드가 프레임 워크에 구현 된 경우이 대답은 쓸모 없게 될 수 있습니다. 이것은 곧 발생하지 않을 것입니다.

일반적으로 "조기에 최적화하지 마십시오"라고 말하기는 그리 좋지 않습니다. 왜냐하면 "조숙 한"시기가 언제인지 알지 못하기 때문입니다. 최적화 할 것인지 아닌지를 결정할 때 고려해야 할 유일한 사항은 "최적화 접근 방법을 제대로 조사 할 시간과 자원이 필요합니까?"입니다. 그렇지 않으면 프로젝트가 더 성숙해질 때까지 또는 성능이 필요할 때까지 기다려야합니다 (실제 필요가 있으면 시간을 벌어 줍니다). 그 때까지 가능한 가장 단순한 작업을 수행하십시오.

원본 코드 :

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

개정 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;
    }

이 개정판은 String.Substring 피하고 대신 StringReader 사용합니다. 주어진 이유는 다음과 같습니다.

편집 : 다음과 같이 단일 패스 구문 분석기를 사용하여 긴 문자열의 성능을 향상시킬 수 있습니다.

음, String.Substring 에 대한 참조 코드를 보면 분명히 이미 "싱글 패스"입니다. 왜 그렇게해서는 안되나요? 바이트 쌍은 surrogate 쌍이 아닌 바이트 수준에서 작동합니다.

그러나 새 문자열을 할당하지만 Convert.ToByte 에 전달할 문자열을 할당해야합니다. 또한 개정판에 제공된 솔루션은 모든 반복 (2 문자 배열)에서 또 다른 객체를 할당합니다. 그 할당을 루프 외부에 안전하게 두어 배열을 재사용하여이를 피할 수 있습니다.

    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

예!

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

재미있어! (But optimize with moderation.)




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



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 an compact, performance junkies will scream about it using enumerators. You can get peak performance with an improved version of Tomolak's original answer:

public static string ByteArrayToString(byte[] ba)   
{   
   StringBuilder hex = new StringBuilder(ba.Length * 2);   

   for(int i=0; i < ga.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.




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.




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.




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



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



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:

  1. The penalties for indexing into a string to perform the lookup vs. a char array are significant in the large file test.
  2. The penalties for using a StringBuilder of known capacity vs. a char array of known size to create the string are even more significant.

코드는 다음과 같습니다.

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




BitConverter.ToString 메서드를 사용할 수 있습니다.

byte[] bytes = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256}
Console.WriteLine( BitConverter.ToString(bytes));

산출:

00-01-02-04-08-10-20-40-80-FF

추가 정보 : BitConverter.ToString 메서드 (Byte [])




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.)




암호 코드를 작성할 때 데이터 종속 분기 및 테이블 조회가 런타임에 데이터에 의존하지 않도록 데이터 종속 타이밍이 측면 채널 공격으로 이어질 수 있으므로 데이터 종속 분기 및 테이블 조회를 피하는 것이 일반적입니다.

꽤 빠릅니다.

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

여기에 들어오는 모든 희망을 포기하십시오.

이상한 비트 바이올린에 대한 설명 :

  1. bytes[i] >> 4 는 바이트의 상위 니블을 추출합니다.
    bytes[i] & 0xF 는 바이트의 낮은 니블을 추출합니다.
  2. b - 10
    b < 10 경우 < 0 이며 십진수가됩니다.
    값이 b > 10 경우 >= 0 이며 A 에서 F 까지 A 문자가됩니다.
  3. 부호가있는 32 비트 정수에 i >> 31 을 사용하면 부호 확장으로 인해 부호가 추출됩니다. i >= 0 i < 0-1 이되고 i >= 0 됩니다.
  4. 2)와 3)을 결합하면 (b-10)>>31 은 문자의 경우 0 , 숫자의 경우 -1 이됩니다.
  5. 이 경우를 보면, 마지막 summand는 0 되고, b 는 10-15의 범위에 있습니다. 우리는 A (65)에서 F (70)로 매핑하려고합니다. 이것은 55를 추가하는 것을 의미합니다 ( 'A'-10 ) .
  6. 숫자의 경우를 살펴보면 마지막 summand를 적용하여 b 를 0에서 9까지의 범위를 0 (48)에서 9 (57)까지 매핑합니다. 이것은 -7이되어야 함을 의미합니다 ( '0' - 55 0'- '0' - 55 ).
    이제 -1을 곱하면됩니다. 그러나 -1은 모든 비트가 1로 표시되기 때문에 (0 & -7) == 0 이고 (-1 & -7) == -7 이므로 & -7 대신 사용할 수 있습니다.

몇 가지 추가 고려 사항 :

  • 나는 두 번째 루프 변수를 사용하여 색인을 작성하지 않았으므로 측정 값이 i 에서 계산하는 것이 더 저렴하다는 것을 보여줍니다.
  • 정확히 i < bytes.Length 를 루프의 상한으로 사용하면 JITter가 bytes[i] 경계 검사를 제거 할 수 있으므로 해당 변형을 선택했습니다.
  • int를 b 로하면 불필요한 바이트 간 변환이 가능합니다.



다른 룩업 테이블 기반 접근법. 이것은 니블마다 룩업 테이블 대신에 각 바이트에 대해 하나의 룩업 테이블만을 사용합니다.

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

또한 lookup table에서 ushort , struct{char X1, X2} , struct{byte X1, X2} 를 사용하여이 변종을 테스트했습니다.

컴파일 대상 (x86, X64)에 따라 거의 동일한 성능을 보 였거나이 변형보다 약간 느립니다.

그리고 더 높은 성능을 위해, unsafe 형제 자매 :

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

또는 스트링에 직접 쓰는 것이 좋다고 생각한다면 :

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