如何将字节数组转换为十六进制字符串,反之亦然?
当前回答
将byte[]转换为十六进制字符串-基准测试/性能分析
更新日期:2022-04-17
从.NET 5开始,您应该使用Convert.ToHexString(bytes[])!
using System;
string result = Convert.ToHexString(bytesToConvert);
关于此排行榜和基准
Thymine的比较似乎过时且不完整,尤其是在.NET 5及其Convert.ToHexString之后,所以我决定~~从字节到十六进制字符串的兔子洞~~创建一个新的、更新的比较,其中包含这两个问题的答案中的更多方法。
我使用的是BencharkDotNet,而不是定制的基准测试脚本,这有望使结果更准确。请记住,微观基准测试永远不能代表实际情况,您应该进行测试。
我在AMD Ryzen 5800H的Linux上运行了这些基准测试,内核为5.15.32,内存为2x8 GB DDR4@2133 MHz。请注意,完成整个基准测试可能需要很多时间——在我的机器上大约需要40分钟。
大写输出与小写输出
所有提到的方法(除非另有说明)都只关注UPPERCASE输出。这意味着输出将看起来像B33F69,而不是B33F69。
Convert.ToHexString的输出始终为大写。不过,值得庆幸的是,与ToLower()配合使用时,性能并没有显著下降,尽管这两种不安全的方法都会更快。
在某些方法中(尤其是具有位运算符魔力的方法),有效地将字符串小写可能是一个挑战,但在大多数情况下,将参数X2更改为X2或将映射中的字母从大写更改为小写就足够了。
排行榜
按平均值N=100排序。参考点是StringBuilderForEachByte方法。
Method (means are in nanoseconds) | Mean N=10 | Ratio N=10 | Mean N=100 | Ratio N=100 | Mean N=500 | Ratio N=500 | Mean N=1k | Ratio N=1k | Mean N=10k | Ratio N=10k | Mean N=100k | Ratio N=100k |
---|---|---|---|---|---|---|---|---|---|---|---|---|
StringBuilderAggregateBytesAppendFormat | 364.92 | 1.48 | 3,680.00 | 1.74 | 18,928.33 | 1.86 | 38,362.94 | 1.87 | 380,994.74 | 1.72 | 42,618,861.57 | 1.62 |
StringBuilderForEachAppendFormat | 309.59 | 1.26 | 3,203.11 | 1.52 | 20,775.07 | 2.04 | 41,398.07 | 2.02 | 426,839.96 | 1.93 | 37,220,750.15 | 1.41 |
StringJoinSelect | 310.84 | 1.26 | 2,765.91 | 1.31 | 13,549.12 | 1.33 | 28,691.16 | 1.40 | 304,163.97 | 1.38 | 63,541,601.12 | 2.41 |
StringConcatSelect | 301.34 | 1.22 | 2,733.64 | 1.29 | 14,449.53 | 1.42 | 29,174.83 | 1.42 | 307,196.94 | 1.39 | 32,877,994.95 | 1.25 |
StringJoinArrayConvertAll | 279.21 | 1.13 | 2,608.71 | 1.23 | 13,305.96 | 1.30 | 27,207.12 | 1.32 | 295,589.61 | 1.34 | 62,950,871.38 | 2.39 |
StringBuilderAggregateBytesAppend | 276.18 | 1.12 | 2,599.62 | 1.23 | 12,788.11 | 1.25 | 26,043.54 | 1.27 | 255,389.06 | 1.16 | 27,664,344.41 | 1.05 |
StringConcatArrayConvertAll | 244.81 | 0.99 | 2,361.08 | 1.12 | 11,881.18 | 1.16 | 23,709.21 | 1.15 | 265,197.33 | 1.20 | 56,044,744.44 | 2.12 |
StringBuilderForEachByte | 246.09 | 1.00 | 2,112.77 | 1.00 | 10,200.36 | 1.00 | 20,540.77 | 1.00 | 220,993.95 | 1.00 | 26,387,941.13 | 1.00 |
StringBuilderForEachBytePreAllocated | 213.85 | 0.87 | 1,897.19 | 0.90 | 9,340.66 | 0.92 | 19,142.27 | 0.93 | 204,968.88 | 0.93 | 24,902,075.81 | 0.94 |
BitConverterReplace | 140.09 | 0.57 | 1,207.74 | 0.57 | 6,170.46 | 0.60 | 12,438.23 | 0.61 | 145,022.35 | 0.66 | 17,719,082.72 | 0.67 |
LookupPerNibble | 63.78 | 0.26 | 421.75 | 0.20 | 1,978.22 | 0.19 | 3,957.58 | 0.19 | 35,358.21 | 0.16 | 4,993,649.91 | 0.19 |
LookupAndShift | 53.22 | 0.22 | 311.56 | 0.15 | 1,461.15 | 0.14 | 2,924.11 | 0.14 | 26,180.11 | 0.12 | 3,771,827.62 | 0.14 |
WhilePropertyLookup | 41.83 | 0.17 | 308.59 | 0.15 | 1,473.10 | 0.14 | 2,925.66 | 0.14 | 28,440.28 | 0.13 | 5,060,341.10 | 0.19 |
LookupAndShiftAlphabetArray | 37.06 | 0.15 | 290.96 | 0.14 | 1,387.01 | 0.14 | 3,087.86 | 0.15 | 29,883.54 | 0.14 | 5,136,607.61 | 0.19 |
ByteManipulationDecimal | 35.29 | 0.14 | 251.69 | 0.12 | 1,180.38 | 0.12 | 2,347.56 | 0.11 | 22,731.55 | 0.10 | 4,645,593.05 | 0.18 |
ByteManipulationHexMultiply | 35.45 | 0.14 | 235.22 | 0.11 | 1,342.50 | 0.13 | 2,661.25 | 0.13 | 25,810.54 | 0.12 | 7,833,116.68 | 0.30 |
ByteManipulationHexIncrement | 36.43 | 0.15 | 234.31 | 0.11 | 1,345.38 | 0.13 | 2,737.89 | 0.13 | 26,413.92 | 0.12 | 7,820,224.57 | 0.30 |
WhileLocalLookup | 42.03 | 0.17 | 223.59 | 0.11 | 1,016.93 | 0.10 | 1,979.24 | 0.10 | 19,360.07 | 0.09 | 4,150,234.71 | 0.16 |
LookupAndShiftAlphabetSpan | 30.00 | 0.12 | 216.51 | 0.10 | 1,020.65 | 0.10 | 2,316.99 | 0.11 | 22,357.13 | 0.10 | 4,580,277.95 | 0.17 |
LookupAndShiftAlphabetSpanMultiply | 29.04 | 0.12 | 207.38 | 0.10 | 985.94 | 0.10 | 2,259.29 | 0.11 | 22,287.12 | 0.10 | 4,563,518.13 | 0.17 |
LookupPerByte | 32.45 | 0.13 | 205.84 | 0.10 | 951.30 | 0.09 | 1,906.27 | 0.09 | 18,311.03 | 0.08 | 3,908,692.66 | 0.15 |
LookupSpanPerByteSpan | 25.69 | 0.10 | 184.29 | 0.09 | 863.79 | 0.08 | 2,035.55 | 0.10 | 19,448.30 | 0.09 | 4,086,961.29 | 0.15 |
LookupPerByteSpan | 27.03 | 0.11 | 184.26 | 0.09 | 866.03 | 0.08 | 2,005.34 | 0.10 | 19,760.55 | 0.09 | 4,192,457.14 | 0.16 |
Lookup32SpanUnsafeDirect | 16.90 | 0.07 | 99.20 | 0.05 | 436.66 | 0.04 | 895.23 | 0.04 | 8,266.69 | 0.04 | 1,506,058.05 | 0.06 |
Lookup32UnsafeDirect | 16.51 | 0.07 | 98.64 | 0.05 | 436.49 | 0.04 | 878.28 | 0.04 | 8,278.18 | 0.04 | 1,753,655.67 | 0.07 |
ConvertToHexString | 19.27 | 0.08 | 64.83 | 0.03 | 295.15 | 0.03 | 585.86 | 0.03 | 5,445.73 | 0.02 | 1,478,363.32 | 0.06 |
ConvertToHexString.ToLower() | 45.66 | - | 175.16 | - | 787.86 | - | 1,516.65 | - | 13,939.71 | - | 2,620,046.76 | - |
结论
ConvertToHexString方法无疑是目前最快的方法,在我看来,如果您有选择的话,应该始终使用它-它既快速又干净。
using System;
string result = Convert.ToHexString(bytesToConvert);
如果没有,我决定在下面强调另外两种我认为值得使用的方法。我决定不强调不安全的方法,因为这样的代码可能不仅是不安全的,而且我合作过的大多数项目都不允许这样的代码。
值得一提
第一个是LookupPerByteSpan。从这个答案中可以看出,该代码与LookupPerBytebyCodesInChaos中的代码几乎相同。这是最快且不安全的基准方法。原始版本和本版本之间的区别在于,对更短的输入使用堆栈分配(最多512字节)。这使得该方法在这些输入上快10%左右,但在较大的输入上慢5%左右。由于我使用的大多数数据都比大数据短,所以我选择了这个。LookupSpanPerByteSpan也非常快,但与所有其他方法相比,其ReadOnlySpan<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);
}
第二个是LookupAndShiftAlphabetSpanMultiply。首先,我想提一下,这是我的创作。然而,我相信这种方法不仅速度很快,而且很容易理解。速度来自于C#7.3中发生的变化,其中声明的ReadOnlyPan<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);
}
源代码
所有方法的源代码、基准和这个答案都可以在GitHub上的Gist中找到。
其他回答
还有XmlWriter.WriteBinHex(请参见MSDN页面)。如果需要将十六进制字符串放入XML流中,这非常有用。
下面是一个独立的方法来了解它的工作原理:
public static string ToBinHex(byte[] bytes)
{
XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
xmlWriterSettings.ConformanceLevel = ConformanceLevel.Fragment;
xmlWriterSettings.CheckCharacters = false;
xmlWriterSettings.Encoding = ASCIIEncoding.ASCII;
MemoryStream memoryStream = new MemoryStream();
using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings))
{
xmlWriter.WriteBinHex(bytes, 0, bytes.Length);
}
return Encoding.ASCII.GetString(memoryStream.ToArray());
}
另一种基于查找表的方法。该方法只为每个字节使用一个查找表,而不是为每个半字节使用查找表。
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);
}
我还使用查找表中的ushort、struct{char X1,X2}、struct{byte X1,X2}测试了这个变体。
根据编译目标(x86、X64)的不同,它们要么具有大致相同的性能,要么稍慢于此变体。
为了获得更高的性能,其不安全的兄弟:
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;
}
我今天遇到了同样的问题,我遇到了以下代码:
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);
}
来源:论坛帖子byte[]数组到十六进制字符串(见PZahra的帖子)。我稍微修改了一下代码,删除了0x前缀。
我对代码进行了一些性能测试,它几乎比使用BitConverter.ToString()快八倍(根据patridge的帖子,速度最快)。
static string ByteArrayToHexViaLookupPerByte2(byte[] bytes)
{
var result3 = new uint[bytes.Length];
for (int i = 0; i < bytes.Length; i++)
result3[i] = _Lookup32[bytes[i]];
var handle = GCHandle.Alloc(result3, GCHandleType.Pinned);
try
{
var result = Marshal.PtrToStringUni(handle.AddrOfPinnedObject(), bytes.Length * 2);
return result;
}
finally
{
handle.Free();
}
}
在我的测试中,这个函数总是不安全实现之后的第二个条目。
不幸的是,测试台不太可靠。。。如果你多次运行它,列表会被打乱,以至于谁知道在不安全之后哪个才是最快的!它没有考虑预热、jit编译时间和GC性能影响。我很想重写它以获得更多信息,但我真的没有时间。
另一种方法是使用stackaloc来降低GC内存压力:
static string ByteToHexBitFiddle(byte[] bytes)
{
var c = stackalloc char[bytes.Length * 2 + 1];
int b;
for (int i = 0; i < bytes.Length; ++i)
{
b = bytes[i] >> 4;
c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
b = bytes[i] & 0xF;
c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
}
c[bytes.Length * 2 ] = '\0';
return new string(c);
}
推荐文章
- 实体框架核心:在上一个操作完成之前,在此上下文中开始的第二个操作
- 如何为构造函数定制Visual Studio的私有字段生成快捷方式?
- 如何使用JSON确保字符串是有效的JSON。网
- 使用C返回一个数组
- AppSettings从.config文件中获取值
- 通过HttpClient向REST API发布一个空体
- 如何检查IEnumerable是否为空或空?
- 自动化invokerrequired代码模式
- 在c#代码中设置WPF文本框的背景颜色
- 在c#中,什么是单子?
- c#和Java中的泛型有什么不同?和模板在c++ ?
- 向对象数组添加属性
- c#线程安全快速(est)计数器
- 如何将此foreach代码转换为Parallel.ForEach?
- 如何分裂()一个分隔字符串到一个列表<字符串>