为什么这个IEnumerable扩展方法比另一个(更简单的)扩展方法(只迭代输入)慢得多

本文关键字:方法 扩展 迭代 输入 另一个 为什么 更简单 IEnumerable | 更新日期: 2023-09-27 18:20:04

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

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生成所有序列的交集集,然后返回结果。CCD_ 2简单地对第一序列进行迭代。

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

结果如下:

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

通过直接返回source.First()FooB可以更快,无论如何,我使用ILSpy反编译了Distinct方法,并发现了完全相同的foreach yield返回循环:

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);  

为什么这个IEnumerable扩展方法比另一个(更简单的)扩展方法(只迭代输入)慢得多

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

如果您将其更改为

    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事件。实际上你确实把苹果比作桔子。您的聚合调用基本上没有任何作用。

改为使用:

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

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

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