Why is this IEnumerable extension method much slower than the other (simpler) extension method (which only repeats input)?

I have a console application that contains two methods:

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

      

What it does: Both take a sequence of sequences, FooA

create a set of intersections of all of them, and then return the result. FooB

just repeat the first sequence.

What I don't understand: more than 10 times slower than , but actually much simpler (no method call ). FooB

FooA

FooB

Intersect()

Here are the results:

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

      

FooB

could be much faster by returning directly source.First()

, anyway, I decompiled the method Distinct

using ILSpy and found exactly the same return loop of foreach exit:

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

      

Also: in the code I am using, I cannot return source.First()

(I am getting CS1622 ). What I am showing here is actually much simpler code that I removed for debugging.

Here's the code I'm using for testing:

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

      

+3


source to share


3 answers


The reason you are measuring such a big difference is that the call to Aggregate just returns your original list, since there are no elements to aggregate, because there is only one element in your list.

If you change it to

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

      

Just one item like you:

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

      



But with two points:

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

      

A is now much slower. The difference in the first example is due to the array allocation, which caused many more CPU cycles.

    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

      

These are GC SelectocationTick ETW events that are thrown by the garbage collector. In fact, you have compared apples to oranges. Your general call did nothing at all.

+5


source


Use instead:



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

      

0


source


FooA

does not call at all Intersect

. There is only one item in the sequence. Aggregate

just returns it. There is nothing to collect.

FooB

moves all elements of the first sequence. It takes time. This will take much longer than just returning the first sequence, for example FooA

.

0


source







All Articles