【问题标题】:Why is Linq significantly slower?为什么 Linq 明显变慢了?
【发布时间】:2018-07-19 08:08:12
【问题描述】:

我尝试使用 Linq 和非 Linq 方法做同样的事情,发现 Linq 速度明显较慢 (~3000x)。

这是为什么呢?

Linq方式:

for (int i = 0; i < totalElements; i += stepSize)
{
    var currentBlock = testList
        .Skip(i)
        .Take(stepSize);

    result.Add(currentBlock.Sum());
}

result.ToList();

非Linq方式:

for (int i = 0; i < totalElements; i += stepSize)
{
    var currentBlock = testList.GetRange(i, stepSize);

    result2.Add(currentBlock.Sum());
}

result2.ToList();

结果:

  • 方法:Linq,耗时:26667 ms,元素:1000000,步长:100

  • 方法:GetRange,耗时:9 毫秒,元素:1000000,步长:100

根据要求提供完整的源代码:

static void Main(string[] args)
{
    var totalElements = 1000000;
    var testList = new List<int>(totalElements);
    var rand = new Random();

    // Initialize the list to random integers between 1 and 1000
    for (int i = 0; i < totalElements; i++)
    {
        testList.Add(rand.Next(1, 1000));
    }

    var result = new List<int>();
    var stepSize = 100;
    var stp = new Stopwatch();

    stp.Start();
    for (int i = 0; i < totalElements; i += stepSize)
    {
        var currentBlock = testList
            .Skip(i)
            .Take(stepSize);

        result.Add(currentBlock.Sum());
    }

    result.ToList();
    stp.Stop();

    Console.WriteLine($"Method: Linq, Time taken: {stp.ElapsedMilliseconds} ms, Elements: {totalElements}, Step Size: {stepSize}");

    stp.Reset();

    var result2 = new List<int>();
    stp.Start();

    for (int i = 0; i < totalElements; i += stepSize)
    {
        var currentBlock = testList.GetRange(i, stepSize);

        result2.Add(currentBlock.Sum());
    }

    result2.ToList();
    stp.Stop();

    Console.WriteLine($"Method: GetRange, Time taken: {stp.ElapsedMilliseconds} ms, Elements: {totalElements}, Step Size: {stepSize}");
}

【问题讨论】:

  • 只有很小的 sn-ps 很难判断或进一步调查。请提供minimal reproducible example
  • 我不认为这是一般“linq 性能”问题的重复。 26 秒对 9 毫秒是极端的。发生了一些奇怪的事情,可能在我们看不到的代码中。
  • @JonSkeet 我已经添加了我用于测试的完整源代码
  • SkipGetRange 根本不等效,它们的工作方式非常不同。确实,Skip 可能应该在source 实现IList&lt;T&gt; 的情况下进行优化,类似于其他IEnumerable 扩展方法的实现方式。不知道他们为什么不这样做......
  • 我得到了更适度的差异:~800ms vs ~7ms dotnetfiddle.net/QhjkVA

标签: c# performance linq


【解决方案1】:

GetRange 使用 Skip()。它总是从头开始枚举。你想要的是一个函数,它可以将你的序列分成多个块,而不需要对序列进行超出实际需要的迭代。

这意味着如果您只想要第一个 Chunk,则函数不应迭代超过此 Chunk,如果我想要第 9 个 Chunk 之后的第 10 个 Chunk,则不应从头开始迭代。

这个扩展功能怎么样?

public static IEnumerable<IEnumerable<Tsource>> ToChuncks<TSource>(
    this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                 // while there are elements left
    {   // still something to chunk
        // yield return a chunk
        yield return source.Take(chunkSize); // return a chunk of chunkSize

        // remove the chunk from the source
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}

此函数反复检查源序列中是否还有剩余内容。如果是这样,它 yield 返回一个数据块并从您的源中删除该块。

这样,您的完整源代码最多迭代两次:一次是迭代块中的元素,一次是迭代块。

【讨论】:

    【解决方案2】:

    问题在于Skip 的工作方式,这与GetRange 完全不同。 Skip 总是从枚举的开头开始,这意味着您正在执行以下操作:

    Iteration #1: Skip 0
    Iteration #2: Skip 1 * step
    Iteration #3: Skip 2 * step
    Iteration #4: Skip 3 * step
    Iteration #5: Skip 4 * step
    ....
    Iteration #1.000: Skip 9.999 * step
    

    如果您对 1.000.000 个元素和 step100 进行数学运算,您会得到:

    sum = 1 + 2 + 3 + .... + 9.999 = 9.999 * (9.999 + 1) / 2 = 49.995.000
    total elements skipped: 49.995.000 * 100 = 4.999.500.000
    

    所以,您的 Linq 版本有大量的 4.999.500.000 不必要的迭代。

    这里的一个好问题是:为什么Skip 没有针对source 实现IList&lt;T&gt; 的情况进行优化,因为很明显,这是可能的。

    【讨论】:

    猜你喜欢
    • 2012-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多