我写了以下代码,将字节数组data
转换为字符串数组hex
,每个条目包含32个字节作为十六进制字符串,以将它们写入文件。
byte[] data = new byte[4*1024*1024];
string[] hex = data.Select((b) => b.ToString("X2")).ToArray();
hex = Enumerable.Range(0, data.Length / 32).Select((r) => String.Join(" ", hex.Skip(r * 32).Take(32))).ToArray(); // <= This line takes forever
问题是,尽管生成的文件小于20MB,但仍需要花费几分钟(!)才能完成。因此,我尝试对其进行优化,并提出了以下建议:
byte[] data = new byte[4*1024*1024];
string[] hex = new string[4*1024*1024/32];
for (var i = 0; i <= hex.Length - 1; i++)
{
var sb = new System.Text.StringBuilder();
sb.Append(data[i * 32].ToString("X2"));
for (var k = 1; k <= 32 - 1; k++)
{
sb.Append(' ');
sb.Append(data[i * 32 + k].ToString("X2"));
}
hex[i] = sb.ToString();
}
此版本的功能相同,但是要快几个数量级(133 ms vs 8分钟)。
我的问题是我不太了解原始版本为什么这么慢。我查看了String.Join()
的{{3}},它看起来与我的改进版本非常相似。
我喜欢将LINQ用于这种想法,因为您可以很轻松地解决各种问题,并且由于它的惰性评估,我认为在大多数情况下它是有效的。因此,我想知道我在这里缺少什么以改善我将来对LINQ的使用。
另一方面,我不知道它的编写速度可能会更高,但这实际上不是重点,因为第二个版本对于仅用于调试目的的功能足够快。
答案 0 :(得分:5)
我的问题是我不太了解为什么原始版本这么慢。
这是这部分:
hex.Skip(r * 32)
.Skip()
必须按照顺序。它不会直接跳到正确的索引。换句话说,对于数组中的每32个字节,您将从头开始遍历整个数组,直到到达当前块的开头。这是Shlemiel the Painter的情况。
还可以通过使用ArraySegment
类型,Array.Copy()
或Span<string>
来提高原始代码的速度。您还可以编写自己的类似linq的"Chunk()"运算符,以从原始IEnumerable
返回32字节的序列,或使用此非常简单的Segment()
方法:
public static IEnumerable<T> Segment<T>(this T[] original, int start, int length)
{
length = start + length;
while (start < length)
yield return original[start++];
}
这会将原始代码更改如下:
byte[] data = new byte[4*1024*1024];
string[] hex = data.Select((b) => b.ToString("X2")).ToArray();
hex = Enumerable.Range(0, data.Length / 32).Select((r) => String.Join(" ", hex.Segment(r * 32,32))).ToArray();
并且为了娱乐,使用我之前链接的Chunk()
实现:
byte[] data = new byte[4*1024*1024];
var hex = data.Select(b => b.ToString("X2"))
.Chunk(32)
.Select(c => string.Join(" ", c))
.ToArray(); //only call ToArray() if you *really* need the array. Often the enumerable is enough.
使用String.Create()
byte[] data = new byte[4*1024*1024];
char[] hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F' };
var hex = data.Chunk(32)
.Select(c => string.Create(95, c, (r, d) => {
int i = 0;
foreach(byte b in d)
{
r[i*3] = hexChars[((b & 0xf0) >> 4)];
r[(i*3) + 1] = hexChars[(b & 0x0f)];
if (i*3 < 92) r[(i*3) + 2] = ' ';
i++;
}
}))
.ToArray();
您还应该查看此BitConverter.ToString()
重载。
我很想看看每个基准测试如何。
答案 1 :(得分:2)
.NET Framework 的Take
实现不对类型IList
的源进行任何优化,因此,对于大型列表或数组重复调用时,它会变得非常慢。 .NET Core includes these optimizations的相应实现,因此它表现得相当不错(与手动编码的循环相当)。