为什么此LINQ查询的速度如此之慢?

时间:2019-11-20 16:47:28

标签: c# .net performance linq

我写了以下代码,将字节数组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的使用。

另一方面,我不知道它的编写速度可能会更高,但这实际上不是重点,因为第二个版本对于仅用于调试目的的功能足够快。

2 个答案:

答案 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的相应实现,因此它表现得相当不错(与手动编码的循环相当)。

相关问题