2014-08-31 1 views
3

У меня есть консольное приложение, которое содержит два метода:Почему этот метод расширения IEnumerable намного медленнее, чем другой (более простой) метод расширения (который только повторяет ввод)?

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 просто повторите первую последовательность.

То, что я не понимаю: FooB более чем в 10 раз медленнее, чем FooA, в то время как FooB на самом деле гораздо более простой (не вызов Intersect() метода).

Вот результаты:

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

FooB может быть намного быстрее, возвращаясь непосредственно source.First(), во всяком случае я декомпилирован Distinct метода с использованием ILSpy и нашел точно такую ​​же цикл Еогеасп обратного выхода:

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

Так что «Интерсект» никогда не называется. Хорошая точка зрения. Я об этом не думал. Он не объясняет, будет ли он все же намного быстрее, чем 'FooB', однако – tigrou

+0

См. Мой ответ, где разница. –

ответ

5

Причина, почему вы измеряете такую ​​большую разницу в том, что совокупный вызов просто возвращает свой первоначальный список, так как нет элементов для агрегирования, потому что в вашем списке есть только один элемент.

Если вы измените его на

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 

А означает гораздо медленнее, в настоящее время. Разница в первом примере объясняется распределением массива, которое вызывало гораздо больше циклов ЦП.

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 SelectocationTick ETW, которые выбрасываются сборщиком мусора. Фактически вы сравнили яблоки с апельсинами. Ваш Агрегатный вызов в основном ничего не делал.

+0

Вы правы. Aggregate не возвращает 'IEnumerable ', а 'TSource' напрямую. – tigrou

0

Использования вместо:

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

FooA не звонит Intersect. В последовательности есть только один элемент. Aggregate просто возвращает его. Нечего собирать.

FooB обходит все элементы первой последовательности. Это требует времени. Это займет гораздо больше времени, чем просто возвращение первой последовательности, например, FooA.