Peter Bucher - Mein Experiment, meine Spielereien, meine Welt...   ·   Stefan Falz   ·   Jürgen Gutsch   ·   Golo Roden   ·   ASP.NET Zone   ·   Microsoft ASP.NET
Willkommen bei ASP.NET Zone. Anmelden | Registrieren | Hilfe

Richtig testen, oder: Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?

Einleitung

Der schweizer Kollege Daniel Schädler hat einen Blogpost mit dem Titel “Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?” veröffentlicht.

Nur schnell eine Korrektur zum Titel, die LINQ-Syntax wird – wie hier schon gezeigt – schlussendlich in die Erweiterungsmethoden-Syntax kompiliert.
Ich habe den Titel so übernommen, damit klar ist, dass das eine Antwort auf seinen Post darstellt.

Daniel hat sich die Frage gestellt, was wohl schneller ist: Listen per foreach, über LINQ oder die Erweiterungsmethoden Syntax für LINQ filtern.

Nun, ich wusste natürlich schon vorher, dass die Resultate nur ganz wenig auseinander liegen dürfen, weil:

  1. Wird die LINQ-Syntax in den Erweiterungsmethoden-Syntax übersetzt (Nahezu gleich).
  2. Arbeitet LINQ intern auch mit den Iteratoren, wie sie in foreach genutzt werden. Siehe Enumerable-Klasse im Reflector.

Abgesehen davon nutze ich diese Antwort in Form eines Blogposts einfach mal dazu, wie ich Geschwindigkeiten teste, auch wenn es in diesem Fall für mich theoretisch klar war, das alle in etwa gleich schnell sind.
Es braucht für einen Geschwindigkeitstest nicht unbedingt eine grafische Anwendung, auch wenn das natürlich nicht schlecht ist. Ich habe mir den Aufwand nicht gemacht und mich auf das Wesentliche konzentriert: Das Testen.

Verfälschte Tests und wie sie verhindert werden können

Der Test von Daniel liefert leider das falsche Resultat, sogar ein sehr verfälschtes.
Das liegt daran, das die Ergebnisse von LINQ bzw. den Erweiterungsmethoden als IEnumerable<T> zurückgegeben werden und diese zeitverzögert ausgeführt werden (deferred Execution).

Das heisst der ganze Code der die IEnumerable<T> Instanz liefert, wird erst ausgeführt, wenn das Ergebnis tatsächlich durchlaufen wird.
Dies kann bspw. mit <Menge>.Count(), <Menge>.ToList(), … angestossen werden.

Erst dann, und wirklich erst dann wird der Code überhaupt ausgeführt und braucht dann eben auch länger.

Auch wenn dies geschehen ist, bleibt das Ergebnis von Daniel noch verfälscht, denn eine Testiteration kann durch mehrere Faktoren verfälscht werden.
Sei das eine einmalige Kompilation einer Abfrage zur Laufzeit oder durch diversen Hintergrundprozessen die genau in dem Moment mehr Leistung benötigen als beim vorherigen Testkandidaten.

Es heisst also: Mehrere Iterationen einer Operation die getestet werden will, ausführen.

Darüber hinaus ist es noch so, dass das Gesamtergebnis auch noch bei mehreren Iterationen der zu testenden Operation verfälscht sein kann, weil eben die erste ausgeführte Operation verfälscht ist.

Um da einen Mittelwert zu bekommen, macht man über die ganzen Testläufe nochmals mehrere Testläufe.
Das hört sich jetzt womöglich ein bisschen verwirrend an, jedoch meine ich einfach gesagt nur folgendes:

  • Eine Operation: Einträge aus einer Menge filtern
  • Mehrere Iterationen einer Operation: Die Operationen mehrmals ausführen
  • Das ganze für alle Testkandidaten mehrmals ausführen

Somit kann man sehr sicher sein, dass die Ergebnisse der verschiedenen Kandidaten realistisch miteinander vergleichbar sind, nicht aber dass die Werte 1:1 dem entsprechen, wie sie dann auch in der Produktivumgebung auftreten, denn dort kommen wieder andere Faktoren dazu.

Nur das keine Missverständisse auftreten: Natürlich ist dann ein solcher Test auch für die Praxis aussagekräftig, nur wird er nicht 100% gleich ausfallen, wenn man die Zeit stoppt.

Als Testhilfe würde ich nicht DateTime.Now, etc.. benutzen, da dies zu ungenau ist. Das meines Wissens genauste Instrument das ohne P/Invoke-Aufrufe in unmanaged Code verfügbar ist, heisst Stopwatch und ist im System.Diagnostics-Namespace zu finden.

Mittels P/Invoke Aufrufen lässt sich eine noch genauere Messung vornehmen, jedoch ist das dann vorallem für Echtzeitanwendungen sinnvoll. Den Einsatz sieht man in einem myCSharp-Post für einen Framecounters unter XNA.

Mittels der Eigenschaft <Stopwatch>.ElapsedMilliseconds lassen sich die Millisekunden auslesen, die seit dem Start- und Stopaufruf vergangen sind, das reicht uns für den Test und ist auch aussagekräftig und vorstellbar, was bei den von Daniel genutzten Ticks m.E. nicht der Fall ist.

Für die Tests nutze ich eine Methode namens SpeedTest die einen Action-Delegate entgegennimmt den sie n-Mal ausführt.
So kann das Testen zentralisiert werden und ist mit sehr kleinem Aufwand möglich den Test selber zu erweitern, sowie neue Kandidaten hinzuzufügen.

Wenn der Test ausgeführt wird, habe ich Abweichungen von 1 bis 4 Millisekunden und das bei sehr vielen Durchläufen. Diese Abweichungen sind für mich also – wie am Anfang vermutet – irrelevant.

Folgend den Code der ich für meinen Test verwendet habe:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace SpeedTest
{
    public class Program
    {
        static void Main(string[] args)
        {
            int randomDataCount = 1000;
            var persons = GetPersons(randomDataCount);

            int iterations = 1000;
            int runs = 10;

            for (int runNumber = 1; runNumber <= runs; runNumber++)
            {
                Console.WriteLine("Run {0} ---------------", runNumber);

                SpeedTest("LINQ", iterations,
                          () =>
                          {
                              var result = (from person in persons
                                            where person.FirstName.Equals("Peter") && person.Age <= 25
                                            select person).ToList();
                          });

                SpeedTest("LINQ Extension Syntax", iterations,
                          () =>
                          {
                              var result = persons.Where(p => p.FirstName.Equals("Peter") && p.Age <= 25).ToList();
                          });

                SpeedTest("Manual ForEach", iterations,
                          () =>
                          {
                              List<Person> result = new List<Person>();

                              foreach (Person person in persons)
                              {
                                  if (person.FirstName.Equals("Peter") && person.Age <= 25)
                                  {
                                      result.Add(person);
                                  }
                              }
                          });

                Console.WriteLine("End run --------");
                Console.WriteLine(String.Empty);
            }

            Console.Read();
        }

        private static void SpeedTest(string description, int iterations, Action speedTestAction)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            for (int i = 0; i < iterations; i++)
            {
                speedTestAction();
            }

            watch.Stop();

            Console.WriteLine("{0}: {1}ms",
                              description,
                              watch.ElapsedMilliseconds);

            Console.WriteLine(String.Empty);
        }

        private static IEnumerable<Person> GetPersons(int randomDataCount)
        {
            Random random = new Random();
            string[] surnames = new[] { "Peter", "Daniel", "Johannes", "Stefanie", "Hans", "Klaus", "Benjamin" };
            string[] lastnames = new[] { "Bucher", "Hurzeler", "Kübel", "Honegger", "Bock", "Wurst", "Grube" };

            int dataLength = surnames.Length;

            for (int i = 0; i < randomDataCount; i++)
            {
                yield return new Person
                                 {
                                     FirstName = surnames[random.Next(0, dataLength)],
                                     LastName = lastnames[random.Next(0, dataLength)],
                                     Age = random.Next(0, 100)
                                 };
            }
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

Veröffentlicht Mittwoch, 9. September 2009 23:43 von Peter Bucher

Kommentare

# re: Richtig testen, oder: Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?

Hallo Peter,

die Methode mit dem Action-Delegate ist ein schöner Ansatz. Gefällt mir sehr. Vieleicht noch der gemittelten Wert mit in die Ausgabe?

Das Konzept werde ich mir bestimmt "ausleihen" :)

Servus,

Klaus

Donnerstag, 10. September 2009 07:58 by klaus_b

# re: Richtig testen, oder: Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?

Hallo Peter,

'Verfälschte Tests', dein Ansatz zur Zeitmessung ist meiner Meinung nach nur dann zulässig wenn die zu testende Methode oft genug aufgerufen wird.

Aber für Methoden welche nur einmal ( oder einige wenige male) aufgerufen werden sorgt dein Ansatz für verfälschte um nicht zu sagen falsche Ergebnisse.

Für Zeitmessung sollte also immer beide Fälle untersucht werden.

Gruß,

Uwe

Donnerstag, 10. September 2009 08:52 by Uwe Furtner

# re: Richtig testen, oder: Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?

Hallo zusammen

@Klaus

Danke.

Ja ein finaler Mittelwert wäre sicherlich noch wünschenswert.

Allerdings habe ich so schon gesehen, dass sich mehrere Durchläufe nicht viel nehmen.

@Uwe

Die zu testende Methode wird ja 1000 Mal aufgerufen und das dann noch 10 Mal (obwohl das keinen Unterschied mehr macht).

Von daher verstehe ich nicht wirklich, wieso da was verfälscht sein sollte.

Mit beiden Fällen meinst du einmal nur mit einem Aufruf und einmal mit mehreren und dies vergleichen?

In der Praxis gibt es sowieso mehrere parallele Zugriffe, wie es hier auch getestet wurde.

Aber vielleicht verstehe ich dich auch falsch?

Donnerstag, 10. September 2009 11:05 by Peter Bucher

# re: Richtig testen, oder: Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?

@Peter

Mein Einwand war mehr grundsätzlicher Natur, unabhängig vom Einsatzgebiet der zu testenden Methode.

Wird die Methode in der Applikation genügend oft aufgerufen so macht dein Ansatz durchaus Sinn, wird die Methode aber nur einmal z.B. zum Applikationsstart aufgerufen so liefert mir dein Ansatz eine falsche information über den Zeitbedarf.

Gruß,

Günter

Freitag, 11. September 2009 10:31 by Uwe Furtner

# re: Richtig testen, oder: Was ist schneller… Foreach, LAMBDA Expressions, oder LINQ?

Hallo Uwe (Günter?)

Alles klar, dann habe ich dich richtig verstanden.

Das stimmt natürlich!

Danke und ein schönes Wochenende! :-)

Freitag, 11. September 2009 11:35 by Peter Bucher

# Was ist schneller: Foreach, LAMBDA Expressions, oder LINQ

LINQ hier und LINQ da.. die eierlegende Wollmilchsau. Oder ...

Mittwoch, 16. Juni 2010 11:34 by Thomys Blog
Anonyme Kommentare sind nicht zugelassen