c# do - Was ist die effizienteste Schleife in c #?





while foreach (2)


Die Antwort ist die meiste Zeit ist es egal. Die Anzahl der Elemente in der Schleife (selbst das, was man als "große" Anzahl von Elementen bezeichnen könnte, sagen wir zu Tausenden) wird sich nicht auf den Code auswirken.

Wenn Sie dies in Ihrer Situation als Engpass identifizieren, adressieren Sie es auf jeden Fall, aber Sie müssen zuerst den Engpass identifizieren.

Allerdings müssen bei jedem Ansatz einige Dinge berücksichtigt werden, die ich hier skizzieren werde.

Lassen Sie uns zuerst ein paar Dinge definieren:

  • Alle Tests wurden unter .NET 4.0 auf einem 32-Bit-Prozessor ausgeführt.
  • TimeSpan.TicksPerSecond auf meinem Rechner = 10.000.000
  • Alle Tests wurden in separaten Unit-Test-Sessions durchgeführt, nicht in derselben (um mögliche Garbage-Collections nicht zu stören, etc.)

Hier sind einige Helfer, die für jeden Test benötigt werden:

Die MyObject Klasse:

public class MyObject
{
    public int IntValue { get; set; }
    public double DoubleValue { get; set; }
}

Eine Methode zum Erstellen einer List<T> beliebiger Länge von MyClass Instanzen:

public static List<MyObject> CreateList(int items)
{
    // Validate parmaeters.
    if (items < 0) 
        throw new ArgumentOutOfRangeException("items", items, 
            "The items parameter must be a non-negative value.");

    // Return the items in a list.
    return Enumerable.Range(0, items).
        Select(i => new MyObject { IntValue = i, DoubleValue = i }).
        ToList();
}

Eine Aktion, die für jedes Element in der Liste ausgeführt wird (erforderlich, weil Methode 2 einen Delegaten verwendet und ein Aufruf an etwas vorgenommen werden muss , um die Auswirkung zu messen):

public static void MyObjectAction(MyObject obj, TextWriter writer)
{
    // Validate parameters.
    Debug.Assert(obj != null);
    Debug.Assert(writer != null);

    // Write.
    writer.WriteLine("MyObject.IntValue: {0}, MyObject.DoubleValue: {1}", 
        obj.IntValue, obj.DoubleValue);
}

Eine Methode zum Erstellen eines TextWriter der in einen null Stream schreibt (im Grunde eine Datensenke):

public static TextWriter CreateNullTextWriter()
{
    // Create a stream writer off a null stream.
    return new StreamWriter(Stream.Null);
}

Und lassen Sie uns die Anzahl der Elemente auf eine Million festlegen (1.000.000, die ausreichend hoch sein sollten, um dies im Allgemeinen zu erreichen, haben alle ungefähr die gleiche Auswirkung auf die Leistung):

// The number of items to test.
public const int ItemsToTest = 1000000;

Kommen wir zu den Methoden:

Methode 1: foreach

Der folgende Code:

foreach(var item in myList) 
{
   //Do stuff
}

Kompiliert in das Folgende:

using (var enumerable = myList.GetEnumerable())
while (enumerable.MoveNext())
{
    var item = enumerable.Current;

    // Do stuff.
}

Dort ist einiges los. Sie haben die Methodenaufrufe (und möglicherweise nicht gegen die IEnumerator<T> oder IEnumerator Schnittstellen, da der Compiler die Duck-Typisierung in diesem Fall respektiert) und Ihr // Do stuff wird in diese während der Struktur gehisst.

Hier ist der Test, um die Leistung zu messen:

[TestMethod]
public void TestForEachKeyword()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        foreach (var item in list)
        {
            // Write the values.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Foreach loop ticks: {0}", s.ElapsedTicks);
    }
}

Die Ausgabe:

Foreach-Schleife tickt: 3210872841

Methode 2: .ForEach Methode für List<T>

Der Code für die .ForEach Methode in List<T> sieht etwa so aus:

public void ForEach(Action<T> action)
{
    // Error handling omitted

    // Cycle through the items, perform action.
    for (int index = 0; index < Count; ++index)
    {
        // Perform action.
        action(this[index]);
    }
}

Beachten Sie, dass dies funktionell äquivalent zu Methode 4 ist. Mit einer Ausnahme wird der Code, der in die for Schleife geholt wird, als Delegat übergeben. Dies erfordert eine Dereferenzierung, um zu dem Code zu gelangen, der ausgeführt werden muss. Während die Leistung der Delegierten seit .NET 3.0 verbessert wurde, ist dieser Overhead vorhanden.

Es ist jedoch vernachlässigbar. Der Test zur Messung der Leistung:

[TestMethod]
public void TestForEachMethod()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        list.ForEach(i => MyObjectAction(i, writer));

        // Write out the number of ticks.
        Debug.WriteLine("ForEach method ticks: {0}", s.ElapsedTicks);
    }
}

Die Ausgabe:

Für jede Methode Ticks: 3135132204

Das ist tatsächlich ~ 7,5 Sekunden schneller als mit der foreach Schleife. Nicht völlig überraschend, da es direkten Array-Zugriff statt IEnumerable<T> .

Denken Sie jedoch daran, dies bedeutet 0,0000075740637 Sekunden pro Artikel gespeichert wird. Das ist es nicht wert für kleine Listen von Gegenständen.

Methode 3: while (myList.MoveNext())

Wie in Methode 1 gezeigt, ist dies genau das , was der Compiler tut (mit dem Zusatz der using Anweisung, was eine gute Übung ist). Sie gewinnen hier nichts, indem Sie den Code selbst abwickeln, den der Compiler sonst erzeugen würde.

Für Tritte, lass es uns trotzdem tun:

[TestMethod]
public void TestEnumerator()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    // Get the enumerator.
    using (IEnumerator<MyObject> enumerator = list.GetEnumerator())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        while (enumerator.MoveNext())
        {
            // Write.
            MyObjectAction(enumerator.Current, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
    }
}

Die Ausgabe:

Aufzählungsschleife ticks: 3241289895

Methode 4: for

In diesem speziellen Fall werden Sie etwas schneller, da der Listenindexer direkt zum zugrunde liegenden Array geht, um die Suche durchzuführen (das ist ein Implementierungsdetail, übrigens, es gibt nichts zu sagen, dass es keine Baumstruktur sein kann) Unterstützen der List<T> oben).

[TestMethod]
public void TestListIndexer()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle by index.
        for (int i = 0; i < list.Count; ++i)
        {
            // Get the item.
            MyObject item = list[i];

            // Perform the action.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("List indexer loop ticks: {0}", s.ElapsedTicks);
    }
}

Die Ausgabe:

Listen-Indexer-Schleife ticks: 3039649305

Der Ort, wo dies einen Unterschied machen kann , sind Arrays. Arrays können vom Compiler abgewickelt werden, um mehrere Objekte gleichzeitig zu verarbeiten.

Anstatt zehn Iterationen eines Elements in einer Schleife mit zehn Elementen auszuführen, kann der Compiler dies in fünf Iterationen von zwei Elementen in einer Schleife mit zehn Elementen auflösen.

Ich bin jedoch nicht sicher, dass dies tatsächlich passiert (ich muss mir die IL und die Ausgabe der kompilierten IL ansehen).

Hier ist der Test:

[TestMethod]
public void TestArray()
{
    // Create the list.
    MyObject[] array = CreateList(ItemsToTest).ToArray();

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle by index.
        for (int i = 0; i < array.Length; ++i)
        {
            // Get the item.
            MyObject item = array[i];

            // Perform the action.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
    }
}

Die Ausgabe:

Array-Loop-Ticks: 3102911316

Es sollte angemerkt werden, dass Resharper einen out-of-the-box-Vorschlag mit einem Refactoring anbietet, um das Obige for Anweisungen zu foreach Anweisungen zu ändern. Das soll nicht heißen, dass das richtig ist, aber die Basis ist, den Betrag der technischen Schuld im Code zu reduzieren.

TL; DR

Sie sollten sich wirklich nicht mit der Leistung dieser Dinge beschäftigen, es sei denn, Tests in Ihrer Situation zeigen, dass Sie einen echten Engpass haben (und Sie müssen eine große Anzahl von Elementen haben, um Einfluss zu haben).

Im Allgemeinen sollten Sie das am besten foreach In diesem Fall ist Methode 1 ( foreach ) der foreach Weg.

Es gibt eine Reihe von verschiedenen Möglichkeiten, die gleiche einfache Schleife durch die Objekte eines Objekts in c # zu erreichen.

Das hat mich dazu gebracht, mich zu fragen, ob es irgendeinen Grund gibt, sei es Leistung oder Benutzerfreundlichkeit, um auf dem anderen zu verwenden. Oder liegt es nur an persönlichen Vorlieben?

Nimm ein einfaches Objekt

var myList = List<MyObject>; 

Nehmen wir an, das Objekt ist gefüllt und wir wollen über die Objekte iterieren.

Methode 1.

foreach(var item in myList) 
{
   //Do stuff
}

Methode 2

myList.Foreach(ml => 
{
   //Do stuff
});

Methode 3

while (myList.MoveNext()) 
{
  //Do stuff
}

Methode 4

for (int i = 0; i < myList.Count; i++)
{
  //Do stuff   
}

Was ich mich gefragt habe, ist, dass jedes dieser Dinge auf dieselbe Sache kompiliert wird? Gibt es einen klaren Leistungsvorteil, wenn man einen über den anderen verwendet?

oder ist das nur auf die persönlichen Vorlieben beim Programmieren zurückzuführen?

Habe ich welche vermisst?




In your comparison between IEnumerable<int> and IEnumerable<double> you don't need to worry - if you pass the wrong type your code won't compile anyway.

There's no concern about type-safety, as var is not dynamic. It's just compiler magic and any type unsafe calls you make will get caught.

Var is absolutely needed for Linq:

var anonEnumeration =
    from post in AllPosts()
    where post.Date > oldDate
    let author = GetAuthor( post.AuthorId )
    select new { 
        PostName = post.Name, 
        post.Date, 
        AuthorName = author.Name
    };

Now look at anonEnumeration in intellisense and it will appear something like IEnumerable<'a>

foreach( var item in anonEnumeration ) 
{
    //VS knows the type
    item.PostName; //you'll get intellisense here

    //you still have type safety
    item.ItemId;   //will throw a compiler exception
}

The C# compiler is pretty clever - anon types generated separately will have the same generated type if their properties match.

Outside of that, as long as you have intellisense it makes good sense to use var anywhere the context is clear.

//less typing, this is good
var myList = new List<UnreasonablyLongClassName>();

//also good - I can't be mistaken on type
var anotherList = GetAllOfSomeItem();

//but not here - probably best to leave single value types declared
var decimalNum = 123.456m;




c# loops for-loop foreach while-loop