如何将字节数组转换为十六进制字符串,反之亦然?

ale*_*nsc 1313 c# arrays hex

如何将字节数组转换为十六进制字符串,反之亦然?

Tom*_*lak 1289

或者:

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();
}
Run Code Online (Sandbox Code Playgroud)

要么:

public static string ByteArrayToString(byte[] ba)
{
  return BitConverter.ToString(ba).Replace("-","");
}
Run Code Online (Sandbox Code Playgroud)

还有更多的变种,例如这里.

反向转换将如下所示:

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;
}
Run Code Online (Sandbox Code Playgroud)

使用Substring是结合使用的最佳选择Convert.ToByte.有关更多信息,请参阅此答案.如果你需要更好的性能,你必须避免Convert.ToByte在丢弃之前SubString.

  • 因为一个字节是两个半字节,所以任何有效表示字节数组的十六进制字符串都必须具有偶数字符数.不应该在任何地方添加0 - 添加一个将假设有潜在危险的无效数据.如果有的话,如果十六进制字符串包含奇数个字符,StringToByteArray方法应抛出FormatException. (84认同)
  • 老实说 - 直到它大幅降低性能,我倾向于忽略这一点,并相信运行时和GC来处理它. (27认同)
  • 你正在使用SubString.这个循环不会分配大量的字符串对象吗? (19认同)
  • @DavidBoike问题与"如何处理可能被剪切的流值"无关"它在谈论一个字符串.String myValue = 10.ToString("X"); myValue是"A"而不是"0A".现在去读取字符串回到字节,哎呀你打破它. (11认同)
  • @00jt你必须假设F == 0F.它与0F相同,或输入被剪裁,F实际上是你没有收到的东西的开始.这取决于你的上下文做出这些假设,但我相信一个通用函数应该拒绝奇数字符作为无效而不是为调用代码做出这种假设. (7认同)
  • 如果十六进制字符数为奇数,则 StringToByteArray() 会失败。通过在前面填充奇数字符串“0”,可以轻松解决此问题。 (2认同)
  • 有关不使用Substring的版本,请参见http://stackoverflow.com/a/14332574/22656. (2认同)

pat*_*dge 464

绩效分析

注:截至2015-08-20的新领导者.

我通过一些粗略的Stopwatch性能测试运行了各种转换方法,一个带有随机句子的运行(n = 61,1000次迭代)和一个带有Project Gutenburg文本的运行(n = 1,238,957,150次迭代).以下是结果,大致从最快到最慢.所有测量均以刻度(10,000滴= 1毫秒)为单位,所有相关注释与[最慢] StringBuilder实施进行比较.对于使用的代码,请参阅下面或测试框架repo,其中我现在维护用于运行此代码的代码.

放弃

警告:不要将这些属性用于任何混凝土; 它们只是样本数据的样本运行.如果您确实需要一流的性能,请在代表您的生产需求的环境中测试这些方法,并使用代表您将使用的数据.

结果

查找表已经领先于字节操作.基本上,有一些形式的预计算任何给定的半字节或字节将以十六进制表示.然后,当你翻阅数据时,你只需查看下一部分,看看它会是什么十六进制字符串.然后以某种方式将该值添加到结果字符串输出中.对于长时间的字节操作,一些开发人员可能更难阅读,是表现最好的方法.

您最好的选择仍然是找到一些有代表性的数据并在类似生产的环境中进行尝试.如果您有不同的内存限制,您可能更喜欢使用较少分配的方法,但速度更快但消耗更多内存的方法.

测试代码

随意玩我使用的测试代码.这里包含一个版本,但可以随意克隆存储并添加自己的方法.如果您发现任何有趣的内容或希望帮助改进其使用的测试框架,请提交拉取请求.

  1. 将新的静态方法(Func<byte[], string>)添加到/Tests/ConvertByteArrayToHexString/Test.cs.
  2. 将该方法的名称添加到TestCandidates同一个类中的返回值.
  3. 通过GenerateTestInput在同一个类中切换注释,确保运行所需的输入版本,句子或文本.
  4. 点击F5并等待输出(在/ bin文件夹中也生成HTML转储).
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();
}
Run Code Online (Sandbox Code Playgroud)

更新(2010-01-13)

添加了Waleed对分析的回答.蛮快.

更新(2011-10-05)

添加string.Concat Array.ConvertAll了完整性的变体(需要.NET 4.0).与string.Join版本相同.

更新(2012-02-05)

测试回购包括更多变体,如StringBuilder.Append(b.ToString("X2")).没有打乱任何结果.foreach比例更快{IEnumerable}.Aggregate,但BitConverter仍然获胜.

更新(2012-04-03)

添加了Mykroft的SoapHexBinary分析答案,获得了第三名.

更新(2013-01-15)

添加了CodesInChaos的字节操作答案,它接管了第一名(在大块文本上大幅度提升).

更新(2013-05-23)

添加了Nathan Moinvaziri的查找答案以及Brian Lambert博客的变体.两者都相当快,但没有在我使用的测试机器上取得领先(AMD Phenom 9750).

更新(2014-07-31)

添加了@ CodesInChaos的新的基于字节的查找答案.它似乎在句子测试和全文测试中都处于领先地位.

更新(2015-08-20)

为这个答案的回购添加了呼吸器的优化和unsafe变体.如果你想在不安全的游戏中玩游戏,那么你可以在短曲目和大文本上获得超过任何先前最佳获胜者的巨大性能提升.

  • 这个答案无意回答什么是"自然的"或普通的问题.我们的目标是为人们提供一些基本的性能基准,因为当你需要进行这些转换时,你往往会做很多事情.如果有人需要原始速度,他们只需在他们想要的计算环境中使用一些适当的测试数据运行基准测试.然后,将该方法转移到一个扩展方法中,在该方法中,您再也不会查看它的实现(例如,`bytes.ToHexStringAtLudicrousSpeed()`). (6认同)
  • 尽管您可以自己编写代码,但我更新了测试代码以包含Waleed答案.抛开所有的脾气暴躁,它要快得多. (5认同)
  • @CodesInChaos完成。在我的测试中,它也赢得了不少胜利。我还不假装不完全了解这两种顶级方法,但是它们很容易被直接交互所隐藏。 (2认同)
  • 刚刚产生了一个基于高性能查询表的实现。它的安全版本比我CPU上的当前领导者快30%。不安全的变体甚至更快。http://stackoverflow.com/a/24343727/445517 (2认同)
  • @Goodies 我发现简单的 Convert.ToBase64String() 在我的测试中非常快(比按字节查找(通过 CodesInChaos)快) - 所以如果有人不关心输出是十六进制的,那是一个快速的 -线路更换。 (2认同)
  • bin 到 hex 可以使用 SIMD 进行矢量化。例如,https://github.com/darealshinji/vectorclass/blob/28d002ca908af1e7b5956c7bffef2bfed7923c6b/special/decimal.h#L835 Agner Fog 的用于 Intel 内在函数 (SSE/AVX) 的向量类库 C++ 包装器。我对 [如何将二进制整数转换为十六进制字符串?](/sf/ask/3767662951/) 的回答是在汇编中,因此可以以同样的努力移植到 C# 内在函数或 C++ 内在函数。通过几次洗牌和移位一次生成 16 或 32B 十六进制数据的向量可以比字节查找表快至少 5 倍。@CodesInChaos (2认同)

Myk*_*oft 234

有一个名为SoapHexBinary的类可以完全满足您的需求.

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();
}
Run Code Online (Sandbox Code Playgroud)

  • SoapHexBinary可从.NET 1.0获得,位于mscorlib中.尽管它有趣的名称空间,它确实提出了问题. (35认同)
  • .NET Core/.NET Standard 不支持 SoapHexBinary... (7认同)
  • 有兴趣在这里看到Mono实现:https://github.com/mono/mono/blob/master/mcs/class/corlib/System.Runtime.Remoting.Metadata.W3cXsd2001/SoapHexBinary.cs (6认同)
  • 很棒的发现!请注意,您需要使用前导0为GetStringToBytes填充奇数字符串,就像其他解决方案一样. (4认同)

Cod*_*aos 137

在编写加密代码时,通常会避免数据相关的分支和表查找,以确保运行时不依赖于数据,因为数据相关的时序可能导致旁路攻击.

它也很快.

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);
}
Run Code Online (Sandbox Code Playgroud)

Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn


进入这里的人,放弃所有的希望

对奇怪的小提琴的解释:

  1. bytes[i] >> 4提取一个字节的高半字节
    bytes[i] & 0xF提取一个字节的低半字节
  2. b - 10
    < 0的价值观b < 10,这将成为一个十进制数字
    >= 0对价值观b > 10,这将成为从信AF.
  3. i >> 31由于符号扩展,使用带符号的32位整数提取符号.这将是-1为了i < 00i >= 0.
  4. 结合2)和3),显示(b-10)>>310用于字母和-1数字.
  5. 查看字母的大小写,最后一个加数变为0,并且b在10到15的范围内.我们想将它映射到A(65)到F(70),这意味着添加55('A'-10).
  6. 查看数字的情况,我们希望调整最后一个加数,使其b从0到9的范围映射到范围0(48)到9(57).这意味着它需要变成-7('0' - 55).
    现在我们可以乘以7.但由于-1表示所有位都是1,我们可以改为使用& -7from (0 & -7) == 0(-1 & -7) == -7.

进一步考虑:

  • 我没有使用第二个循环变量来索引c,因为测量表明从中计算它i更便宜.
  • 完全使用i < bytes.Length循环的上限允许JITter消除边界检查bytes[i],因此我选择了该变体.
  • 创建b一个int允许从字节到字节的不必要的转换.

  • +1在调用那个黑魔法后正确引用你的来源+1.所有人都欢呼克苏鲁. (14认同)
  • 太好了!对于那些需要小写输出的人来说,表达式显然会变为"87 + b +(((b-10)>> 31)& - 39)" (9认同)
  • 和`hex string`到`byte [] array`? (8认同)
  • 字符串到字节[]怎么样? (4认同)
  • @AaA 你说的是“`byte[] array`”,字面意思是一个字节数组的数组,或者`byte[][]`。我只是在开玩笑。 (2认同)

Wil*_*ean 92

如果你想要更多的灵活性BitConverter,但又不想要那些笨重的20世纪90年代风格的显式循环,那么你可以做到:

String.Join(String.Empty, Array.ConvertAll(bytes, x => x.ToString("X2")));
Run Code Online (Sandbox Code Playgroud)

或者,如果您使用的是.NET 4.0:

String.Concat(Array.ConvertAll(bytes, x => x.ToString("X2")));
Run Code Online (Sandbox Code Playgroud)

(后者来自对原帖的评论.)

  • 更短:String.Concat(Array.ConvertAll(bytes,x => x.ToString("X2")) (21认同)
  • 更短:String.Concat(bytes.Select(b => b.ToString("X2")))_ [.NET4] _ (14认同)
  • 只回答问题的一半. (14认同)
  • 为什么第二个需要.Net 4?String.Concat 位于 .Net 2.0 中。 (2认同)
  • 那些“ 90年代风格”的循环通常更快,但是数量可忽略不计,因此在大多数情况下都不重要。仍然值得一提 (2认同)

Cod*_*aos 65

另一种基于查找表的方法.这个每个字节只使用一个查找表,而不是每个半字节的查找表.

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);
}
Run Code Online (Sandbox Code Playgroud)

我还测试了使用这种变型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);
}
Run Code Online (Sandbox Code Playgroud)

或者,如果您认为直接写入字符串是可以接受的:

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;
}
Run Code Online (Sandbox Code Playgroud)

  • @CodesInChaos我想知道现在是否可以使用“Span”而不是“unsafe”? (8认同)
  • 这只是回答问题的一半......从十六进制字符串到字节怎么样? (3认同)
  • 好吧,我要咬牙——无限期地固定“_lookup32Unsafe”,而不是仅仅执行第三个“fixed”语句,并让 GC 在该方法不运行时将数组重新定位到其核心内容,有什么好处? (2认同)

Bag*_*get 61

您可以使用BitConverter.ToString方法:

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

输出:

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

更多信息:BitConverter.ToString方法(Byte [])

  • 只回答问题的一半. (12认同)
  • 答案的第二部分在哪里? (2认同)
  • 我希望 256 转换为“FF”只是一个拼写错误...... (2认同)

Wal*_*ssa 53

我今天刚遇到同样的问题,我遇到了这段代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

来源:论坛帖子byte []数组到十六进制字符串(参见PZahra的帖子).我稍微修改了代码以删除0x前缀.

我对代码进行了一些性能测试,它比使用BitConverter.ToString()快了近八倍(根据patridge的帖子,速度最快).

  • 只回答问题的一半. (7认同)
  • 接受的答案提供了 2 个优秀的 HexToByteArray 方法,它们代表了问题的另一半。Waleed 的解决方案回答了如何在不创建大量字符串的情况下执行此操作的运行问题。 (2认同)

ant*_*riz 32

将 byte[] 转换为十六进制字符串 - 基准/性能分析


更新日期:2022-04-17


从 .NET 5 开始,您应该使用Convert.ToHexString(bytes[])

using System;
string result = Convert.ToHexString(bytesToConvert);
Run Code Online (Sandbox Code Playgroud)

关于此排行榜和基准

Thymine的比较似乎已经过时且不完整,特别是在 .NET 5 及其之后Convert.ToHexString,所以我决定~~陷入字节到十六进制字符串的兔子洞~~创建一个新的、更新的比较,使用更多方法从两个问题的答案中进行比较两个问题。

我使用BenchamrkDotNet而不是定制的基准测试脚本,希望这将使结果更加准确。
请记住,微基准测试永远无法代表实际情况,您应该进行测试。

我在内核为5.15.32 的Linux上运行了这些基准测试,并在配备2x8 GB DDR4 @ 2133 MHz的AMD Ryzen 5800H上运行。 请注意,整个基准测试可能需要很长时间才能完成 - 在我的机器上大约需要 40 分钟。

大写(大写)与小写输出

提到的所有方法(除非另有说明)仅关注大写输出。这意味着输出将类似于B33F69, not b33f69.

的输出Convert.ToHexString始终为大写。不过,值得庆幸的是,与 配合使用时,性能没有任何显着下降ToLower(),尽管unsafe如果您担心的话,两种方法都会更快。

在某些方法中,有效地使字符串小写可能是一个挑战(尤其是那些具有位运算符魔法的方法),但在大多数情况下,在映射中更改参数或将字母从大写更改为小写X2就足够了。x2

排行榜

它是按 排序的Mean N=100。参考点是StringBuilderForEachByte方法。

方法(单位为纳秒) 平均值 N=10 比率N=10 平均值 N=100 比率N=100 平均值 N=500 比率N=500 平均 N=1k 比率N=1k 平均 N=10k 比率N=10k 平均 N=100k 比率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
字符串连接选择 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
字符串连接选择 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
字符串连接数组转换全部 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
每个字节的字符串生成器 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
StringBuilderForEachBytePre分配 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
BitConverter替换 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
查找每半字节 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
查找与移位 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
While属性查找 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
字节操作十进制 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
字节操作十六进制乘法 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
字节操作十六进制增量 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
本地查找时 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
查找和移动字母跨度 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
查找和移位字母跨度乘法 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
逐字节查找 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
查找每字节跨度 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
转换为十六进制字符串 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 -

结论

该方法ConvertToHexString无疑是最快的,在我看来,如果可以选择,应该始终使用它 - 它快速且干净。

using System;

string result = Convert.ToHexString(bytesToConvert);
Run Code Online (Sandbox Code Playgroud)

如果没有,我决定在下面重点介绍另外两种我认为有价值的方法。我决定不突出显示unsafe方法,因为这样的代码可能不仅不安全,而且我参与过的大多数项目都不允许这样的代码。

值得一提

第一个是LookupPerByteSpan。该代码与此答案中CodesInChaos中的
代码几乎相同。这是最快的非方法基准测试。原始版本与此版本之间的区别在于,对较短的输入(最多 512 字节)使用堆栈分配。这使得该方法在这些输入上速度快了约 10%,但在较大输入上速度慢了约 5%。由于我处理的大多数数据都比较短,因此我选择了这一数据。也非常快,但是与所有其他方法相比,其映射的代码大小太大。LookupPerByteunsafeLookupSpanPerByteSpanReadOnlySpan<byte>

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);
}
Run Code Online (Sandbox Code Playgroud)

第二个是LookupAndShiftAlphabetSpanMultiply。首先,我想提一下,这是我的创作。不过,我相信这种方法不仅速度很快,而且简单易懂。速度来自 C# 7.3 中发生的变化,其中ReadOnlySpan<byte>返回常量数组初始化的声明方法new byte {1, 2, 3, ...}被编译为程序的静态数据,因此省略了冗余内存分配。[来源]

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);
}
Run Code Online (Sandbox Code Playgroud)

源代码

所有方法的源代码、基准测试和此答案都可以作为要点在我的 GitHub 上找到。


Tru*_*ems 25

Dotnet 5 更新

要将byte[](字节数组)转换为十六进制string,请使用:

System.Convert.ToHexString

var myBytes = new byte[100];
var myString = System.Convert.ToHexString(myBytes);
Run Code Online (Sandbox Code Playgroud)

要将十六进制转换stringbyte[],请使用:

System.Convert.FromHexString

var myString  = "E10B116E8530A340BCC7B3EAC208487B";
var myBytes = System.Convert.FromHexString(myString);
Run Code Online (Sandbox Code Playgroud)


小智 21

从 .NET 5 RC2 开始,您可以使用:

可以使用带跨度参数的重载。

  • 在.NET 6中,`Convert.ToHexString`在CPU上使用SSSE3指令集,因此不仅像.NET 5中那样使用方便,而且更加[性能](https://github.com/dotnet/runtime/ pull/44111) 用于超过 3 个字节的输入。随着输入大小的增加,性能差异更加明显。 (3认同)

tne*_*tne 19

这是Tomalak非常受欢迎的答案(及后续编辑)修订版4答案.

我将说明这种编辑是错误的,并解释为什么它可以被还原.在此过程中,您可能会了解一些关于某些内部构件的内容,并看到另一个例子,说明过早优化的真正含义以及它如何咬你.

tl; dr:只是使用Convert.ToByte,String.Substring如果你赶时间(下面的"原始代码"),如果你不想重新实现它,它是最好的组合Convert.ToByte.Convert.ToByte如果您需要性能,请使用不使用的更高级的内容(请参阅其他答案).千万不能使用别的以外String.Substring与组合Convert.ToByte,除非有人一些有趣的事情在这个答案的评论说这个.

警告:如果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;
    }
Run Code Online (Sandbox Code Playgroud)

修订版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;
    }
Run Code Online (Sandbox Code Playgroud)

修订版避免String.Substring使用StringReader而是使用.给出的原因是:

编辑:您可以使用单个传递解析器来提高长字符串的性能,如下所示:

那么,看看参考代码String.Substring,它显然已经"单通"了; 为什么不应该呢?它在字节级操作,而不是在代理对上操作.

但它会分配一个新的字符串,但是你需要分配一个传递给它Convert.ToByte.此外,修订版中提供的解决方案在每次迭代时分配另一个对象(双字符数组); 您可以安全地将该分配放在循环之外并重用该数组以避免这种情况.

    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;
    }
Run Code Online (Sandbox Code Playgroud)

每个十六进制numeral表示使用两个数字(符号)的单个八位字节.

但是,为什么要打StringReader.Read两次电话?只需调用它的第二个重载并让它一次读取两个char数组中的两个字符; 并将通话量减少两个.

    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;
    }
Run Code Online (Sandbox Code Playgroud)

您剩下的是一个字符串阅读器,其唯一添加的"值"是_pos您可以自己声明的并行索引(内部)(j例如),冗余长度变量(内部_length)和对输入的冗余引用string(内部_s).换句话说,它没用.

如果您想知道如何Read"读取",只需查看代码,它所做的就是调用String.CopyTo输入字符串.剩下的就是保持账面管理,以维持我们不需要的价值.

所以,已经删除了字符串阅读器,并打电话给CopyTo自己; 它更简单,更清晰,更高效.

    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;
    }
Run Code Online (Sandbox Code Playgroud)

你真的需要一个j以两个平行步长递增的索引i吗?当然不是,只需乘以i2(编译器应该能够优化为加法).

    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;
    }
Run Code Online (Sandbox Code Playgroud)

现在的解决方案是什么样的?完全像在开头一样,只使用String.Substring分配字符串并将数据复制到其中,而不是使用中间数组将十六进制数字复制到其中,然后自己分配字符串并再次从中复制数据数组和字符串(当你在字符串构造函数中传递它时).如果字符串已经在实习池中,则可以优化第二个副本,但String.Substring在这些情况下也可以避免它.

实际上,如果你String.Substring再看一遍,你会发现它使用了一些关于如何构造字符串的低级内部知识来比你通常做的更快地分配字符串,并且它CopyTo直接在那里内联使用相同的代码来避免呼叫开销.

String.Substring

  • 最坏情况:一次快速分配,一次快速复制.
  • 最好的情况:没有分配,没有副本.

手动方法

  • 最坏情况:两次正常分配,一次正常复制,一次快速复制.
  • 最佳情况:一次正常分配,一次正常复制.

结论?如果你想使用Convert.ToByte(String, Int32)(因为你不想自己重新实现这个功能),似乎没有办法击败String.Substring; 你所做的只是在圆圈中运行,重新发明轮子(仅使用次优材料).

请注意,如果您不需要极端性能,使用Convert.ToByteString.Substring是一个非常有效的选择.请记住:如果您有时间和资源来调查它是如何正常工作的,请选择替代方案.

如果有的话Convert.ToByte(char[], Int32),事情就会有所不同(有可能做我上面所描述的并完全避免String).

我怀疑那些通过"避免String.Substring" 来报告性能更好的人也会避免Convert.ToByte(String, Int32),如果你还需要表现,那么你应该这样做.看看无数的其他答案,发现所有不同的方法来做到这一点.

免责声明:我没有反编译最新版本的框架,以验证参考源是最新的,我认为是.

现在,这一切听起来都很好而且合乎逻辑,如果你已经成功到目前为止,这一点甚至是显而易见的.但这是真的吗?

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
Run Code Online (Sandbox Code Playgroud)

是!

对于替补框架的道具鹧to,它很容易入侵.使用的输入是以下SHA-1哈希重复5000次以生成100,000字节长的字符串.

209113288F93A9AB8E474EA78D899AFDBB874355
Run Code Online (Sandbox Code Playgroud)

玩得开心!(但要适度优化.)


drp*_*zen 16

使用查找表也可以解决此问题.这将需要编码器和解码器的少量静态存储器.然而,这种方法会很快:

  • 编码器表512字节或1024字节(如果需要大小写,则为两倍大小)
  • 解码器表256字节或64 KiB(单个字符查找或双字符查找)

我的解决方案使用1024个字节作为编码表,使用256个字节进行解码.

解码

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]));
}
Run Code Online (Sandbox Code Playgroud)

编码

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]];
}
Run Code Online (Sandbox Code Playgroud)

对照

StringBuilderToStringFromBytes:   106148
BitConverterToStringFromBytes:     15783
ArrayConvertAllToStringFromBytes:  54290
ByteManipulationToCharArray:        8444
TableBasedToCharArray:              5651 *
Run Code Online (Sandbox Code Playgroud)

*这个解决方案

注意

在解码过程中,IOException和IndexOutOfRangeException可能会发生(如果一个字符的值太大> 256).应该实现de/encoding流或数组的方法,这只是一个概念证明.

  • 在CLR上运行代码时,可以忽略256字节的内存使用情况。 (2认同)

Cop*_*ick 16

补充@CodesInChaos(反向方法)

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;
}
Run Code Online (Sandbox Code Playgroud)

说明:

& 0x0f 是支持小写字母

hi = hi + 10 + ((hi >> 31) & 7); 是相同的:

hi = ch-65 + 10 + (((ch-65) >> 31) & 7);

为"0" .."9"是相同hi = ch - 65 + 10 + 7;它是hi = ch - 48(这是因为0xffffffff & 7).

对于'A'..'F'它是hi = ch - 65 + 10;(这是因为0x00000000 & 7).

对于'a'..'f',我们必须使用大数字,因此我们必须0通过使用一些位来从默认版本中减去32 & 0x0f.

65是代码 'A'

48是代码 '0'

7是ASCII表()之间'9'和之间的字母数.'A'...456789:;<=>?@ABCD...


Chr*_*s F 9

这是一篇很棒的文章.我喜欢Waleed的解决方案.我还没有通过patridge的测试来运行它,但它看起来非常快.我还需要反向过程,将十六进制字符串转换为字节数组,因此我将其写为Waleed解决方案的逆转.不确定它是否比Tomalak的原始解决方案更快.同样,我也没有通过patridge的测试运行相反的过程.

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;
}
Run Code Online (Sandbox Code Playgroud)

  • Convert.ToByte(topChar + bottomChar)可以写成(byte)(topChar + bottomChar) (3认同)

Mar*_*ark 9

Microsoft 的开发人员提供了一个很好、简单的转换:

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();
}
Run Code Online (Sandbox Code Playgroud)

虽然上面的内容干净紧凑,但性能迷们会因为使用枚举器而尖叫。您可以通过Tomalak 原始答案的改进版本获得最佳性能:

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();   
} 
Run Code Online (Sandbox Code Playgroud)

这是迄今为止我在这里看到的所有例程中最快的。不要只相信我的话...对每个例程进行性能测试并亲自检查其 CIL 代码。

  • 迭代器不是这段代码的主要问题。您应该对“b.ToSting("X2")”进行基准测试。 (3认同)

小智 8

为什么要复杂?这在Visual Studio 2008中很简单:

C#:

string hex = BitConverter.ToString(YourByteArray).Replace("-", "");
Run Code Online (Sandbox Code Playgroud)

VB:

Dim hex As String = BitConverter.ToString(YourByteArray).Replace("-", "")
Run Code Online (Sandbox Code Playgroud)

  • 原因是性能,当您需要高性能解决方案时。:) (2认同)

Ben*_*her 7

不要在这里找到很多答案,但我发现了一个相当优化的(比接受的好4.5倍),十六进制字符串解析器的直接实现.首先,我的测试输出(第一批是我的实现):

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
Run Code Online (Sandbox Code Playgroud)

base64和'BitConverter'd'行用于测试正确性.请注意它们是平等的.

实施:

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;
}
Run Code Online (Sandbox Code Playgroud)

我尝试了一些东西unsafe并将(明显多余的)字符到半字节if序列移动到另一个方法,但这是它得到的最快.

(我承认这回答了问题的一半.我觉得字符串 - > byte []转换的代表性不足,而字节[] - >字符串角度似乎被很好地覆盖了.因此,这个答案.)


Ali*_*hid 7

.NET 5 添加了Convert.ToHexString方法。

对于那些使用旧版本 .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,
}
Run Code Online (Sandbox Code Playgroud)

改编自 .NET 存储库 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


Geo*_*aph 6

Waleed Eissa 代码的反函数(十六进制字符串到字节数组):

    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;
    }
Run Code Online (Sandbox Code Playgroud)

支持小写的 Waleed Eissa 函数:

    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);
    }
Run Code Online (Sandbox Code Playgroud)


Ros*_*sei 6

测试:十六进制字符串到字节数组

我注意到大多数测试都是在将字节数组转换为十六进制字符串的函数上执行的。因此,在这篇文章中,我将重点关注另一方面:将十六进制字符串转换为字节数组的函数。如果您只对结果感兴趣,可以跳到摘要部分。测试代码文件在帖子末尾提供。

标签

我想从接受的答案(由 Tomalak)命名该函数 StringToByteArrayV1,或者将其快捷方式命名为 V1。其余函数将以同样的方式命名:V2、V3、V4、...等。

参与功能索引

正确性测试

我通过传递 1 个字节的所有 256 个可能值,然后检查输出以查看是否正确来测试正确性。结果:

  • V18 存在以“00”开头的字符串问题(请参阅 Roger Stewart 对它的评论)。除此之外它通过了所有测试。
  • 如果十六进制字符串字母为大写:所有函数均成功通过
  • 如果十六进制字符串字母为小写,则以下函数失败:V5_1、V5_2、v7、V8、V15、V19

注意:V5_3 解决了这个问题(V5_1 和 V5_2)

性能测试

我已经使用秒表类完成了性能测试。

  • 长弦性能
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)
Run Code Online (Sandbox Code Playgroud)
  • V18 的长弦性能
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)
Run Code Online (Sandbox Code Playgroud)
  • 短弦性能
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)
Run Code Online (Sandbox Code Playgroud)

测试代码

在使用以下代码https://github.com/Ghosticollis/performance-tests/blob/main/MTestPerformance.cs中的任何内容之前,最好先阅读本文中的免责声明部分

概括

我建议使用以下函数之一,因为其性能良好,并且支持大小写:

这是 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;
}
Run Code Online (Sandbox Code Playgroud)

免责声明

警告:我没有适当的测试知识。这些原始测试的主要目的是快速概述所有已发布功能的优点。如果您需要准确的结果,请使用适当的测试工具。

最后,我想说,我是 stackoverflow 的新手,如果我的帖子有遗漏,抱歉。如果您能提出意见来完善这篇文章,我们将不胜感激。


Pur*_*ome 5

扩展方法(免责声明:完全未经测试的代码,顺便说一句......):

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

等.. 使用Tomalak 的三种解决方案之一(最后一种是字符串上的扩展方法)。


Mar*_*ius 5

安全版本:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

不安全版本适合那些喜欢表现并且不怕不安全的人.ToHex快35%,FromHex快10%.

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

BTW 对于每次调用转换函数错误时初始化字母表的基准测试,字母必须是const(对于字符串)或静态只读(对于char []).然后,byte []到string的基于字母的转换变得和字节操作版本一样快.

当然,必须在Release(优化)中编译测试,并关闭调试选项"Suppress JIT optimization"(如果代码必须是可调试的,则"启用我的代码"相同).


Ale*_*lis 5

老派人的最快方法......想念你的指针

    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;
    }
Run Code Online (Sandbox Code Playgroud)