我创建了3种不同的方法,使用以下格式将byte[]
转换为十六进制字符串:{ 0xx2, ... }
(Working Demo)
using System;
using System.Diagnostics;
using System.Text;
public class Program
{
public delegate string ParseMethod(byte[] Msg);
public class Parser
{
public string Name { get; private set;}
public ParseMethod Handler { get; private set; }
public Parser(string name, ParseMethod method)
{
Name = name;
Handler = method;
}
}
public static void Main()
{
Parser HexA = new Parser("ToHexA", ToHexA);
Parser HexB = new Parser("ToHexB", ToHexB);
Parser HexC = new Parser("ToHexC", ToHexC);
TestCorrectness(HexA);
TestCorrectness(HexB);
TestCorrectness(HexC);
Console.WriteLine("Small Message Performance:");
TestPerformance(HexA, MsgSize: 10, Iterations: 1000000);
TestPerformance(HexB, MsgSize: 10, Iterations: 1000000);
TestPerformance(HexC, MsgSize: 10, Iterations: 1000000);
Console.WriteLine();
Console.WriteLine("Large Message Performance:");
TestPerformance(HexA, MsgSize: 500, Iterations: 1000000);
TestPerformance(HexB, MsgSize: 500, Iterations: 1000000);
TestPerformance(HexC, MsgSize: 500, Iterations: 1000000);
Console.WriteLine();
}
private static void TestCorrectness(Parser parser)
{
Console.WriteLine("Testing Correctness of \"{0}(byte[] Msg)\"", parser.Name);
Console.WriteLine("\"{0}\"", parser.Handler(new byte[]{}));
Console.WriteLine("\"{0}\"", parser.Handler(new byte[]{ 97 }));
Console.WriteLine("\"{0}\"", parser.Handler(new byte[]{ 97, 98, 99, 0, 100 }));
Console.WriteLine();
}
private static void TestPerformance(Parser parser, int MsgSize, int Iterations)
{
Stopwatch sw = new Stopwatch();
sw.Reset();
byte[] Msg = new byte[MsgSize];
sw.Start();
for (uint i = 0; i < Iterations; ++i)
{
parser.Handler(Msg);
}
sw.Stop();
Console.WriteLine("Performance for \"{0}\", {1}", parser.Name, sw.Elapsed);
}
private static string ToHexA(byte[] buffer)
{
return
(
"{ 0x" +
BitConverter.ToString(buffer).ToLower()
.Replace("-", ", 0x") +
" }"
)
.Replace(" 0x }", "}");
}
private static string ToHexB(byte[] buffer)
{
if (buffer.Length == 0) { return "{}"; }
const string Preamble = "{ 0x";
const string Delimiter = ", 0x";
const string Epilogue = " }";
string Msg = Preamble + buffer[0].ToString("x2");
for (int i = 1; i < buffer.Length; ++i)
{
Msg += Delimiter + buffer[i].ToString("x2");
}
return Msg += Epilogue;
}
private static string ToHexC(byte[] buffer)
{
if (buffer.Length == 0) { return "{}"; }
const string Preamble = "{ 0x";
const string Delimiter = ", 0x";
const string Epilogue = " }";
StringBuilder HexOut = new StringBuilder(
Preamble.Length +
(Delimiter.Length * (buffer.Length - 1)) +
(2 * buffer.Length) +
Epilogue.Length
);
HexOut.Append(Preamble);
HexOut.Append(buffer[0].ToString("x2"));
for (int i = 1; i < buffer.Length; ++i)
{
HexOut.Append(Delimiter);
HexOut.Append(buffer[i].ToString("x2"));
}
HexOut.Append(Epilogue);
return HexOut.ToString();
}
}
运行此代码我得到以下性能统计信息
Small Message Performance:
Performance for "ToHexA", 00:00:01.3078387
Performance for "ToHexB", 00:00:01.6939201
Performance for "ToHexC", 00:00:01.2997903
Large Message Performance:
Performance for "ToHexA", 00:00:32.5230253
Performance for "ToHexB", 00:04:23.4798762
Performance for "ToHexC", 00:00:56.2404684
我发现令人惊讶的是,ToHexA
(相对于ToHexC
)使用较长的消息执行得更快,而使用较短的消息则执行速度较慢。据我了解,Replace()
,+
和ToLower()
都必须执行创建/复制操作,因为字符串是不可变的。
与此同时,我怀疑StringBuilder的开销可能会使它不太适合更短的消息,但在这种情况下执行速度比ToHexA
更快。
我唯一预料到的是与ToHexB
相关的性能损失,这种情况介于两个世界最糟糕的场景中......
那么,这两种方法及其性能特征是怎么回事?
我对两种性能密切的方法的确切性能特征并不特别感兴趣。我知道这在不同的设备,架构,后台进程等方面会有所不同,并且存在更好的测试方法来控制变量而不是
我提出这个问题的目的是为了更好地理解为什么两个明显不同的(在性能方面)方法会叠加他们的方式。接受的答案以满足这个问题的方式解释ToString
的机制。
这个问题的一个限制是输出的确切格式是特定的(我们只能冥想为什么,我认为它是非常标准的)由于某些原因而不是C#/ .NET中的标准输出格式之一;正是这种独特的格式导致了这样一个问题:对StringBuilder
答案 0 :(得分:2)
您的方法C将是最快的连接方法,但是每次调用ToString("x2")
时,您仍然会产生垃圾。
除此之外,带有格式字符串的ToString
重载对于这样的事情来说是非常慢的,因为他们首先必须处理格式字符串才能进行实际工作,这种格式字符串处理重复多次。太糟糕了,没有Converter<byte, string> GetToString(string format)
处理格式字符串一次并返回有状态转换对象。
无论如何,byte.ToString()
在这里很容易避免。
static readonly char[] digits = "0123456789abcdef".ToCharArray();
private static string ToHexD(byte[] buffer)
{
if (buffer.Length == 0) { return "{}"; }
const string Preamble = "{ 0x";
const string Delimiter = ", 0x";
const string Epilogue = " }";
int expectedLength =
Preamble.Length +
(Delimiter.Length * (buffer.Length - 1)) +
(2 * buffer.Length) +
Epilogue.Length;
StringBuilder HexOut = new StringBuilder(expectedLength);
HexOut.Append(Preamble);
HexOut.Append(digits[buffer[0] >> 4]).Append(digits[buffer[0] & 0x0F]);
for (int i = 1; i < buffer.Length; ++i)
{
HexOut.Append(Delimiter);
HexOut.Append(digits[buffer[i] >> 4]).Append(digits[buffer[i] & 0x0F]);
}
HexOut.Append(Epilogue);
return HexOut.ToString();
}
对Append(char)
的两次调用也应该比Append(string)
的一次调用快,但这比在格式字符串上节省时间要重要得多。
正如预期的那样,这种方法可以轻松地胜过BitConverter
- 然后 - Replace()
方法(大约只有两倍)。
为了进一步提高速度,可以通过准备每个长度为256的前导数字和尾数位查找表来避免计算buffer[i] >> 4
和buffer[i] & 0x0f
。但按位操作非常快;在许多缺少大型L1数据缓存的微控制器上,增加查找表的大小会比按位运算成本更高。
答案 1 :(得分:0)
如上所述,这是一个高度调整的功能,专门用于此请求的输出。请原谅“神奇的价值观”:),我把它作为POC扔了:),但它非常快,这是重点。它避免了所有的字符串操作,StringBuilder,调用其他方法等等。
private unsafe static string ToHexA2(byte[] buffer)
{
int length = buffer.Length;
int num = (length * 6) + 2;
char[] array = new char[num];
int num2 = 0;
fixed (char* ptr = array)
{
*(long*)ptr = 0x0020007B;
for (int i = 2; i < num; i += 6)
{
byte b = buffer[num2++];
*(long*)(ptr + i) = 0x00780030;
*(long*)(ptr + i + 2) = digits[b >> 4] | (digits[b & 0x0F] << 16);
*(long*)(ptr + i + 4) = 0x0020002C;
}
*(long*)(ptr + num - 2) = 0x007D0020;
}
return new string(array, 0, array.Length);
}