【问题标题】:Why IEnumerable slow and List is fast?为什么 IEnumerable 慢而 List 快?
【发布时间】:2013-11-10 10:39:41
【问题描述】:

遇到了这段代码。

var dic = new Dictionary<int, string>();
for(int i=0; i<20000; i++)
{
    dic.Add(i, i.ToString());
}

var list = dic.Where(f => f.Value.StartsWith("1")).Select(f => f.Key);//.ToList(); //uncomment for fast results 
Console.WriteLine(list.GetType());
var list2 = dic.Where(f => list.Contains(f.Key)).ToList();
Console.WriteLine(list2.Count());

所以当 .ToList() 被评论时它很慢,当没有评论时 - 它很快。可重现here这怎么解释?我是否应该始终制作所有 ToList() 以确保速度(即在哪种情况下 IEnumerable 会更可取)?注意我说的只是 linq to objects,我知道 linq to sql 的惰性和其他东西。

【问题讨论】:

  • 你是怎么看时间的?
  • @JoelCoehoorn 挂钟

标签: c# linq


【解决方案1】:

因为当您没有 .ToList() 调用时,list2 实例化将遍历字典中每个项目的整个 list 可枚举。所以如果你使用延迟执行,你会从 O(n) 变为 O(n^2)。

【讨论】:

    【解决方案2】:

    这是因为延迟执行:当您注释掉ToList 时,枚举是通过评估字典中每个项目的过滤器序列产生的。但是,当您执行ToList 时,序列会在内存中“物化”,因此所有评估都只执行一次。

    没有ToList 的第二个Where 背后的逻辑是这样的:

    // The logic is expanded for illustration only.
    var list2 = new List<KeyValuePair<int,string>>();
    foreach (var d in dict) {
        var list = new List<int>();
        // This nested loop does the same thing on each iteration,
        // redoing n times what could have been done only once.
        foreach (var f in dict) {
            if (f.Value.StartsWith("1")) {
                list.Add(f.Key);
            }
        }
        if (list.Contains(d.Key)) {
            list2.Add(d);
        }
    }
    

    ToList 的逻辑如下所示:

    // The list is prepared once, and left alone
    var list = new List<int>();
    foreach (var f in dict) {
        if (f.Value.StartsWith("1")) {
            list.Add(f.Key);
        }
    }
    var list2 = new List<KeyValuePair<int,string>>();
    // This loop uses the same list in all its iterations.
    foreach (var d in dict) {
        if (list.Contains(d.Key)) {
            list2.Add(d);
        }
    }
    

    如您所见,ToList 将具有两个大小为n 的嵌套循环的O(n^2) 程序转换为具有两个大小为n 的顺序循环的O(2*n)

    【讨论】:

      【解决方案3】:

      这是由延迟执行引起的。 IEnumerable 不必是静态集合。一般来说,它是一些数据源(在您的情况下为dic)+ 导致最终集合的所有方法和表达式(Where、Contains 等)。

      .ToList() 执行所有这些方法和表达式并生成最终结果。

      因此,如果您使用 ToList(),它会生成一个标准的 .NET 列表(整数数组)并对该列表执行所有操作。

      如果您不调用 ToList()(或任何其他 To-method),则可以多次枚举 IEnumerable。

      【讨论】:

        【解决方案4】:

        LINQ 使用延迟执行
        除非您调用.ToList(),否则查询的结果永远不会存储在任何地方;相反,它会在您每次迭代结果时重新迭代查询。

        通常,这要快得多;通常没有理由先将所有结果存储在内存中。

        但是,您的代码会反复迭代查询;每次调用 Where() 回调一次。

        您应该用Join() 调用而不是ToList() 替换该行,这将比任何一种方法都快。

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-06-15
        • 2012-01-25
        • 1970-01-01
        • 2011-08-07
        • 2015-07-24
        相关资源
        最近更新 更多