2015-12-29 2 views
0

В настоящее время я работаю над чем-то, что требует иерархии, такой как дизайн (примерно так же, как в unity ... На самом деле, я просто делаю копию единства на вершине единства как опыт обучения), что требует много вставки/перемещения объектов внутри списка. По этой причине я решил пойти с LinkedList для хранения всех объектов иерархии (первый раз, когда я когда-либо пользовался списком ListList).Iterating LinkedListed Performance C#

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

(я не специалист по настройке тестов производительности, но этот вид настройки обычно дает мне хорошее представление о производительности различий)

Тест: С 10 целых чисел в списке, а другой с 100.000

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 
using System.Diagnostics; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 

    private static Stopwatch timer = new Stopwatch(); 

    static void Main(string[] args) 
    { 
     /// Initialization 

     var _linkedList = new LinkedList<int>(); 

     for (int i = 0; i < 10; i++) 
      _linkedList.AddLast(i); 


     /// Test 1 

     timer.Start(); 

     for (var _node = _linkedList.First; _node != _linkedList.Last; _node = _node.Next) 
      FuncFoo(_node.Value); 

     timer.Stop(); 

     Console.WriteLine("For Loop: " + timer.Elapsed); 

     timer.Reset(); 


     /// Test 2 

     timer.Start(); 

     foreach (var _int in _linkedList) 
      FuncFoo(_int); 

     timer.Stop(); 

     Console.WriteLine("Foreach Loop: " + timer.Elapsed); 

     timer.Reset(); 


     /// Test 3 

     timer.Start(); 

     var _listNode = _linkedList.First; 

     while (_listNode != _linkedList.Last) 
     { 
      FuncFoo(_listNode.Value); 
      _listNode = _listNode.Next; 
     } 

     timer.Stop(); 

     Console.WriteLine("While Loop: " + timer.Elapsed); 

     timer.Reset(); 


     ///End 

     Console.Write("Press any key to continue..."); 

     Console.ReadKey(); 
    } 

    private static void FuncFoo(int _num) 
    { 
     _num = (int)Math.Sqrt(1 + 2 + 3 + 4 + 5) * _num; 
    } 
    } 
} 

Результаты: 10 целых чисел в списке

For Loop: 0.0002371 
Foreach Loop: 0.0002880 
While Loop: 0.0000002 

Результаты: 100000 целых чисел в списке

For Loop: 0.0013548 
Foreach Loop: 0.0015256 
While Loop: 0.0013436 

Итак, 2 вещи, о которых я не знаю.

  1. Почему цикл for намного медленнее, чем цикл while? Я думал, что цикл for работает как цикл while за кулисами? (Я понимаю, почему цикл foreach немного медленнее во всех случаях)

  2. Почему цикл while становится менее эффективным по мере добавления большего количества элементов в связанный список? Я полагал, что они будут все более или менее оставаться в одном и том же соотношении (например, для foreach loops). Но видеть, что цикл while, казалось бы, теряет производительность (или for/foreach gain performance?) Меня смущает.

Я предполагаю, что мой код, вероятно, не функционировал в качестве теста (В этом случае я хотел бы знать, что я сделал неправильно/почему, так что я знаю, как сделать более надежные тесты в будущем). Но в случае, если это действительный тест, я хотел бы знать, что вызывает эти кажущиеся странными результаты (даже если разница в производительности не изменит мой код).

ответ

2

Прежде всего, сделайте свой тест более представительным, увеличив количество итераций от одного до 1 миллиона. Кроме того, добавить не записанные итераций для каждого теста, чтобы сделать шанс для JIT компилятор оптимизировать наш код:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 
using System.Diagnostics; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 

     private static Stopwatch timer = new Stopwatch(); 

     static void Main(string[] args) 
     { 
      /// Initialization 

      var _linkedList = new LinkedList<int>(); 

      for (int i = 0; i < 10; i++) 
       _linkedList.AddLast(i); 

      for (int x = 0; x < 1000000; x++) 
       for (var _node = _linkedList.First; _node != _linkedList.Last; _node = _node.Next) 
        FuncFoo(_node.Value); 
      for (int x = 0; x < 1000000; x++) 
       foreach (var _int in _linkedList) 
        FuncFoo(_int); 
      for (int x = 0; x < 1000000; x++) 
      { 
       var _listNode = _linkedList.First; 
       while (_listNode != _linkedList.Last) 
       { 
        FuncFoo(_listNode.Value); 
        _listNode = _listNode.Next; 
       } 
      } 

      /// Test 1 

      timer.Start(); 

      for (int x = 0; x < 1000000; x++) 
       for (var _node = _linkedList.First; _node != _linkedList.Last; _node = _node.Next) 
        FuncFoo(_node.Value); 

      timer.Stop(); 

      Console.WriteLine("For Loop: " + timer.Elapsed); 

      timer.Reset(); 


      /// Test 2 

      timer.Start(); 

      for (int x = 0; x < 1000000; x++) 
       foreach (var _int in _linkedList) 
        FuncFoo(_int); 

      timer.Stop(); 

      Console.WriteLine("Foreach Loop: " + timer.Elapsed); 

      timer.Reset(); 


      /// Test 3 

      timer.Start(); 



      for (int x = 0; x < 1000000; x++) 
      { 
       var _listNode = _linkedList.First; 
       while (_listNode != _linkedList.Last) 
       { 
        FuncFoo(_listNode.Value); 
        _listNode = _listNode.Next; 
       } 
      } 

      timer.Stop(); 

      Console.WriteLine("While Loop: " + timer.Elapsed); 

      timer.Reset(); 


      ///End 

      Console.Write("Press any key to continue..."); 

      Console.ReadKey(); 
     } 

     private static void FuncFoo(int _num) 
     { 
      _num = (int)Math.Sqrt(1 + 2 + 3 + 4 + 5) * _num; 
     } 
    } 
} 

А как ничего себе! Существует более фактический результат:

For Loop: 00:00:00.2793502 
Foreach Loop: 00:00:00.3588778 
While Loop: 00:00:00.2660378 

Как мы видим, для цикла и while цикл имеет тот же результат. Это потому, что их код MSIL почти такой же, тоже:

IL_0115: callvirt instance class [System]System.Collections.Generic.LinkedListNode`1<!0> class [System]System.Collections.Generic.LinkedList`1<int32>::get_First() 
IL_011a: stloc.s _listNode 
IL_011c: br.s IL_0136 
// loop start (head: IL_0136) 
    IL_011e: nop 
    IL_011f: ldloc.s _listNode 
    IL_0121: callvirt instance !0 class [System]System.Collections.Generic.LinkedListNode`1<int32>::get_Value() 
    IL_0126: call void ConsoleApplication1.Program::FuncFoo(int32) 
    IL_012b: nop 
    IL_012c: ldloc.s _listNode 
    IL_012e: callvirt instance class [System]System.Collections.Generic.LinkedListNode`1<!0> class [System]System.Collections.Generic.LinkedListNode`1<int32>::get_Next() 
    IL_0133: stloc.s _listNode 
    IL_0135: nop 

    IL_0136: ldloc.s _listNode 
    IL_0138: ldloc.0 
    IL_0139: callvirt instance class [System]System.Collections.Generic.LinkedListNode`1<!0> class [System]System.Collections.Generic.LinkedList`1<int32>::get_Last() 
    IL_013e: ceq 
    IL_0140: ldc.i4.0 
    IL_0141: ceq 
    IL_0143: stloc.s CS$4$0000 
    IL_0145: ldloc.s CS$4$0000 
    IL_0147: brtrue.s IL_011e 
// end loop 

ForEach петля работает дольше, поскольку его механизм создает новый экземпляр Enumerator, который реализует IDisposable и должны быть расположены на конце цикла.Фактический код MSIL выглядит следующим образом:

IL_009c: ldloc.0 
IL_009d: callvirt instance valuetype [System]System.Collections.Generic.LinkedList`1/Enumerator<!0> class [System]System.Collections.Generic.LinkedList`1<int32>::GetEnumerator() 
IL_00a2: stloc.s CS$5$0001 
.try 
{ 
    IL_00a4: br.s IL_00b5 
    // loop start (head: IL_00b5) 
     IL_00a6: ldloca.s CS$5$0001 
     IL_00a8: call instance !0 valuetype [System]System.Collections.Generic.LinkedList`1/Enumerator<int32>::get_Current() 
     IL_00ad: stloc.3 
     IL_00ae: ldloc.3 
     IL_00af: call void ConsoleApplication1.Program::FuncFoo(int32) 
     IL_00b4: nop 

     IL_00b5: ldloca.s CS$5$0001 
     IL_00b7: call instance bool valuetype [System]System.Collections.Generic.LinkedList`1/Enumerator<int32>::MoveNext() 
     IL_00bc: stloc.s CS$4$0000 
     IL_00be: ldloc.s CS$4$0000 
     IL_00c0: brtrue.s IL_00a6 
    // end loop 

    IL_00c2: leave.s IL_00d3 
} // end .try 
finally 
{ 
    IL_00c4: ldloca.s CS$5$0001 
    IL_00c6: constrained. valuetype [System]System.Collections.Generic.LinkedList`1/Enumerator<int32> 
    IL_00cc: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    IL_00d1: nop 
    IL_00d2: endfinally 
} // end handler 

Как вы можете видеть это MSIL полностью отличается тем, что он использует Enumerator.MoveNext метод вместо LinkedListNode.get_Next() и создает попробовать блок, который влияет на производительность, потому что есть затраты, связанные с обработкой исключений на некоторых платформ.

Кроме того, JIT не выполняет оптимизацию на блоках «try», и это также повлияет на вашу производительность.

+0

Выполнение 'foreach' теряется из-за дополнительных накладных расходов в' Enumerator.MoveNext() '. 'LinkedList .Enumerator.Dispose()' ничего не делает, и я считаю, что накладные расходы try/catch также несущественны. – Kimi

+0

Пробные блоки могут повлиять на производительность. В этом случае он может быть не фактическим ботом в общем случае. –

+0

Спасибо, что нашли время, чтобы ответить на него, думаю, я знал меньше, чем я думал, что знаю. Могу ли я спросить, как работает оптимизация (путем запуска большого количества непроверенных версий перед началом работы). К сожалению, я не понимаю кода MSIL или того, как работает JIT, я попытался получить базовое понимание, но я никогда не испытывал ДЕЙСТВИТЕЛЬНО. Мне также интересно, как работает каждый тест в миллион раз, меняет результат (отдельный для предварительной оптимизации). Означает ли это, что невозможно определить скорость каждого отдельного цикла? Или это совсем неважно? – Lolop