Optimierung: For Next vs For Each

    • VB.NET

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von picoflop.

      Optimierung: For Next vs For Each

      Die wohl elementarste Schleife in VB (sowohl classic als auch .Net) dürfte wohl die "For Next" Schleife - auch "Zählschleife" genannt - sein.
      In ihrer einfachsten Form sieht sie so aus:

      VB.NET-Quellcode

      1. For i = 1 To 10
      2. Debug.Print(i.ToString)
      3. Next

      Wir zählen von 1 bis 10 und geben den Wert unserer "Zählvariablen" aus. Sinnlos aber zur Einführung mag das jetzt reichen ;)

      Meistens "machen" wir aber was sinnvolles mit unserem "i". ZB sprechen wir die einzelnen Elemente eines "Arrays" an:

      VB.NET-Quellcode

      1. Dim a() As Integer = {1, 2, 3}
      2. For i = 0 To a.Count - 1
      3. Debug.Print(a(i).ToString)
      4. Next

      Neben Arrays gibt es natürlich noch andere Sachen, die wir über ihren "Index" ansprechen können. Im Prinzip alles, was das "Interface" IEnumerable unterstützt. "Enumerable" bedeutet ja letztlich nichts anderes als "nummerierbar". Sogar Arrays (die ja ganz simple Aufzählungen sind) unterstützen dieses Interface:

      VB.NET-Quellcode

      1. Dim a() As Integer = {1, 2, 3}
      2. Dim ia As IEnumerable(Of Integer) = a
      3. For i = 0 To ia.Count
      4. Debug.Print(ia(i).ToString)
      5. Next

      Wiederum reichlich sinnlos, denn warum sollten wir uns das Interface "ziehen", wenn wir die Elemente auch direkt ansprechen können?
      Das Interface nutzen wir (meist unbemerkt) dann, wenn wir stattdessen das "For Each" Konstrukt verwenden:

      VB.NET-Quellcode

      1. Dim a() As Integer = {1, 2, 3}
      2. For Each i As Integer In a
      3. Debug.Print(i.ToString)
      4. Next

      Wir haben jetzt also keine "Zählvariable" mehr, sondern sagen einfach "nimm jedes Element und ...".
      Was ist jetzt aber "besser". Nun ... das hängt davon ab ;)

      "For Each" erzeugt einen gewissen "Overhead" - es wird also zusätzlicher Code (im hintergrund) erzeugt, um die Aufzählung zu machen. Das bedeutet, dass "For next" immer schneller ist? Nein! Wenn ich eine "statische" Aufzählung habe, dann ist es mit dem Index oft (aber nicht immer, das hängt vom Typ der Aufzählung ab!) schneller - wobei das häufig nur dann zum Tragen kommt, wenn ich SEHR viele Elemente habe und IN der Schleife nur etwas sehr einfaches/schnelles mache. Dh das Schleifenkonstrukt selbst hat einen großen Anteil am Aufwand.

      Aber wenn man zb sowas hat:

      VB.NET-Quellcode

      1. Private Iterator Function Lines(ByVal path As String) As IEnumerable(Of String)
      2. Using sr As New IO.StreamReader(path)
      3. While Not sr.EndOfStream
      4. Dim s As String = sr.ReadLine
      5. Debug.Print("inside! " & s)
      6. Yield s
      7. End While
      8. End Using
      9. End Function

      Was macht das? "Iterator" ist eine neue Funktion (aus dem CTP Async), die es erlaubt - ganz einfach! - in VB.Net EIGENE Aufzählungsfunktionen zu schreiben, wobei natürlich im Net Framework schon diverse Methoden drin sind, die genau so (!) funktionieren.
      Ok. Unsere Funktion nimmt einen "Pfad" (zu einer Textdatei) und liefert jeweils "eine Zeile" zurück.
      Man verwendet das ganze so:

      VB.NET-Quellcode

      1. For Each s As String In Lines("data.txt")
      2. Debug.Print("outside! " & s)
      3. Next

      ("data.txt" ist eine Textdatei, die drei Zeilen enthält in denen jeweils 1,2 und 3 drinstehen)
      Läßt man das ganze laufen, bekommt man folgenden output:

      Quellcode

      1. inside! 1
      2. outside! 1
      3. inside! 2
      4. outside! 2
      5. inside! 3
      6. outside! 3

      Ziemlich trivial - wie man denken könnte ;)

      Aber was passiert, wenn ich nicht "For Each", sondern die "normale" For Next Schleife nehme?

      VB.NET-Quellcode

      1. Dim il As IEnumerable(Of String) = Lines("data.txt")
      2. For i = 0 To il.Count - 1
      3. Debug.Print("outside! " & il(i))
      4. Next

      Unser output sieht dann so aus:

      Quellcode

      1. inside! 1
      2. inside! 2
      3. inside! 3
      4. inside! 1
      5. outside! 1
      6. inside! 1
      7. inside! 2
      8. outside! 2
      9. inside! 1
      10. inside! 2
      11. inside! 3
      12. outside! 3

      Bawoom! Was ist denn JETZT passiert??
      Wir verwenden die .Count Eigenschaft um eine obere Grenze für unsere Zählvariable zu ermitteln. Also muss natürlich erstmal JEDE Zeile gelesen werden, damit man eine ANZAHL hat. Das erklärt die ersten drei inneren Aufrufe.
      Als nächstes sprechen wir jedes einzelne Element an. Um dann das Element Nummer "X" zu finden muss unsere Funktion ALLE Elemente vorher einlesen (und verwerfen, weil sie gar nicht benötigt werden). Und wie man sieht, geht das mit steigender Elementzahl ziemlich rasch nach oben.

      ACHTUNG Mathematik:
      Im ersten Fall für X Elemente wird unsere "inside" Operation genau X Mal ausgeführt. Immer ;)
      Im zweiten Fall:
      X (für die Bestimmung der Anzahl) PLUS (1 + X) * X/2 (Gauss!)
      Wir haben also einen "Overhead" von (X + X^2)/2. Dh. der Aufwand wächst QUADRATISCH! Man stelle sich mal eine Textdatei mit 1 Mio Zeilen vor ...

      Was lernen wir also:
      Wenn wir uns 120% sicher sind, nehmen wir "For i ... next". Wenn wir uns unsicher sind (was da im hintergrund nun WIRKLICH abläuft), nehmen wir "For each"
      Man sehe sich zb mal "System.IO.File.ReadLines" vs "System.IO.File.ReadAllLines" an! Die erste Version liefert nämlich genau unser IENumerable(Of String) zurück und die Verwendung von "For index ... next" wäre da superkontraproduktiv.

      IMHO natürlich und Kommentare willkommen ;)
      1. super Tutorial, wird sicher für einige den RAM-Verbrauch senken.
      2. es gibt den "Befehl" "Yield" unter VB.NET nicht. Eine etwas andere Art wäre:

      VB.NET-Quellcode

      1. Dim tmp As IEnumerable(Of String)
      2. Using sr As New IO.StreamReader(path)
      3. While Not sr.EndOfStream
      4. Dim s As String = sr.ReadLine
      5. Debug.Print("inside! " & s)
      6. tmp.add(s)
      7. End While
      8. End Using
      9. Return tmp