【问题标题】:Why is this IEnumerable extension method much slower than another (more simpler) extension method (that only iterate input)?为什么这个 IEnumerable 扩展方法比另一个(更简单的)扩展方法(只迭代输入)慢得多?
【发布时间】:2014-10-24 20:17:54
【问题描述】:

我有一个包含两种方法的控制台应用程序:

public static IEnumerable<TSource>
          FooA<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
    return source.Aggregate((x, y) => x.Intersect(y));
}

public static IEnumerable<TSource> 
          FooB<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
    foreach (TSource element in source.First())
    {
        yield return element;
    }
}

它的作用:都取一个序列序列,FooA 产生所有它们的交集,然后返回结果。 FooB 只需迭代第一个序列。

我不明白的是:FooBFooA慢10倍以上,而FooB实际上要简单得多(没有调用Intersect()方法)。

这是结果:

00:00:00.0071053 (FooA)
00:00:00.0875303 (FooB)

FooB 可以通过直接返回 source.First() 快很多,反正我使用 ILSpy 反编译了 Distinct 方法并发现完全相同的 foreach 产生返回循环:

private static IEnumerable<TSource> DistinctIterator<TSource>
   (IEnumerable<TSource> source, IEqualityComparer<TSource> comparer)
{
    Set<TSource> set = new Set<TSource>(comparer);
    foreach (TSource current in source)
    {
        if (set.Add(current))
        {
            yield return current;
        }
    }
    yield break;
} 

另外:在我使用的代码中,我无法返回source.First()(我得到CS1622)。我在这里展示的实际上是一个更简单的代码,我为调试而剥离。

这是我用于测试的代码:

List<List<int>> foo = new List<List<int>>();
foo.Add(new List<int>(Enumerable.Range(0, 3000*1000)));

Stopwatch sa = new Stopwatch();
sa.Start();
List<int> la = FooA(foo).ToList();
Console.WriteLine(sa.Elapsed);


Stopwatch sb = new Stopwatch();
sb.Start();
List<int> lb = FooB(foo).ToList();
Console.WriteLine(sb.Elapsed);  

【问题讨论】:

  • 所以Intersect 永远不会被调用。好点子。我没有考虑到这一点。它没有解释它是否仍然比FooB 快很多
  • 查看我的答案,差异来自哪里。

标签: c# linq optimization enumerable


【解决方案1】:

改用:

public static IEnumerable<TSource> FooB<TSource>(this IEnumerable<IEnumerable<TSource>> source) {
    yield return source.First();
}

【讨论】:

    【解决方案2】:

    之所以要测量如此大的差异,是因为聚合调用只是返回您的初始列表,因为没有要聚合的项目,因为您的列表只有一个项目。

    如果你改成

        List<List<int>> foo = new List<List<int>>()
        {
            new List<int>(Enumerable.Range(0, 3000 * 1000)),
            new List<int>(Enumerable.Range(0, 3000 * 1000)),
        };
    

    只有一件像你这样的人:

    A: 00:00:00.0037843
    B: 00:00:00.0514177
    

    但是有两个项目:

    A: 00:00:00.2130628
    B: 00:00:00.0574932
    

    A 现在慢了很多。第一个示例中的差异是由于数组分配确实导致了更多的 CPU 周期。

        AllocationAmount AllocationKind
    B     1CAE0         Small
    B     21E5C         Small
    B     20020         Large
    B     40020         Large
    B     80020         Large
    B    100020         Large
    B    200020         Large
    B    400020         Large
    B    800020         Large
    B   1000020         Large
    A    B71B20         Large
    

    这是垃圾收集器发出的 GC AllocationTick ETW 事件。实际上,您确实将苹果与橙子进行了比较。您的聚合调用基本上什么也没做。

    【讨论】:

    • 你是对的。聚合不直接返回IEnumerable&lt;TSource&gt;,而是直接返回TSource
    【解决方案3】:

    FooA 根本不调用Intersect。序列中只有一个元素。 Aggregate 只是返回它。没有什么可以聚合的。

    FooB 遍历第一个序列的所有元素。这需要时间。这比像FooA 那样只返回第一个序列要花更长的时间。

    【讨论】:

      猜你喜欢
      • 2010-12-26
      • 1970-01-01
      • 2022-01-09
      • 2019-03-17
      • 2022-01-15
      • 2014-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多