Optimierung: if a and b then ?

    • VB.NET

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

      Optimierung: if a and b then ?

      Jeder - na ja, fast jeder ... - kennt die logischen Vergleichsoperatoren "and" und "or". Die sind quasi Basic-Urgestein und auch in VB.Net zu finden. In VB.Net gibt's allerdings auch noch "andalso" und "orelse", was sich etwa nach "und auch" bzw "oder sonst" übersetzen läßt. Die machen grundsätzlich das Gleiche, warum also zwei neue Vokabeln?
      Historisch (VB6 und Vorgänger) ist es immer so gewesen, dass bei Verwendung von "and" und "or" immer ALLE Teile des Ausdrucks "berechnet" (evaluiert) wurden:

      If a > 1 And a < 10 Then

      Jetzt wird erst geprüft, ob a größer als 1 ist und dann noch, ob a kleiner als 10 ist. Wie man sieht ist das "eigentlich" Unsinn, denn wenn a zB Null ist, kann man ja eigentlich aufhören mit prüfen, denn der GESAMTE Ausdruck kann ja nie mehr "true" (wahr) sein.
      Umgekehrtes gilt natürlich für "or". Wenn der erste Ausdruck schon "true" ist, ist alles was danach kommt ziemlich egal, denn "or" liefert ja schon "true" wenn einer der Ausdrücke "true" ist.
      Für genau diese "Abkürzung" gibt es in VB.Net jetzt "AndAlso" und "OrElse". Hierbei wird dann nur solange geprüft, wie es - rein menschlich gesehen - "Sinn" macht. D.h. wenn bei AndAlso der erste Ausdruck schon "false" ist, wird die weitere Überprüfung abgebrochen, da es ja eh nie mehr "true" werden kann. Analog bei "OrElse" wird solange geprüft, bis ein Ausdruck "true" ist, da ja dann der Gesamtausdruck "true" ist.

      Warum hat man nicht einfach das Verhalten von "or" und "and" angepasst? Zum einen aus Kompatibilitätsgründen und weil es manchmal Situationen gibt, in denen auf jeden alle Teile geprüft werden sollen. Das könnte zb sein, wenn man eine Function aufruft (die true oder false zurückliefert) und die bei jeden Durchlauf der Prüfung aufgerufen werden soll - warum auch immer, aber es gibt solche Fälle, wenn auch ggfs selten.

      Was heißt das?
      1. Im Zweifel (!) sollte man statt "and" und "or" immer "andalso" und "orelse" verwenden!
      2. Man sollte seine Ausdrücke so sortieren, dass die wahrscheinlichste Abbruchbedingung (bei andalso -> false und bei orelse -> true) ZUERST geprüft wird.

      Die Verwendung gerade von "AndAlso" hat noch einen weiteren Vorteil, der dann zum Tragen kommt, wenn man mit Objekten arbeitet, die ja ggfs "Nothing" sind.

      VB.NET-Quellcode

      1. dim s as System.IO.Stream
      2. If s IsNot Nothing and s.seek(...) Then
      3. -> Fehler! Denn s ist zwar Nothing, aber dennoch wird der zweite Teil ausgeführt und da gibt's dann den Fehler WEIL s ja nothing ist!
      4. If s IsNot Nothing AndAlso s.Seek() Then
      5. -> Kein Problem! s ist nothing, deswegen wird der zweite Teil gar nicht angefasst!


      Anhang:
      MSIL Listing mit den 4 verschiedenen Varianten.
      Spoiler anzeigen

      ORELSE -->>
      .method private instance void foo() cil managed
      {
      // Code size 25 (0x19)
      .maxstack 1
      .locals init ([0] bool a,
      [1] bool b,
      [2] bool c,
      [3] bool VB$CG$t_bool$S0)
      IL_0000: nop
      IL_0001: ldc.i4.1
      IL_0002: stloc.0
      IL_0003: ldc.i4.0
      IL_0004: stloc.1
      IL_0005: ldc.i4.0
      IL_0006: stloc.2
      IL_0007: ldloc.0
      IL_0008: brtrue.s IL_0010
      IL_000a: ldloc.1
      IL_000b: brtrue.s IL_0010
      IL_000d: ldc.i4.0
      IL_000e: br.s IL_0011
      IL_0010: ldc.i4.1
      IL_0011: stloc.3
      IL_0012: ldloc.3
      IL_0013: brfalse.s IL_0017
      IL_0015: ldc.i4.1
      IL_0016: stloc.2
      IL_0017: nop
      IL_0018: ret
      } // end of method Form1::foo
      <<-- ORELSE

      -->> OR
      .method private instance void foo() cil managed
      {
      // Code size 18 (0x12)
      .maxstack 2
      .locals init ([0] bool a,
      [1] bool b,
      [2] bool c,
      [3] bool VB$CG$t_bool$S0)
      IL_0000: nop
      IL_0001: ldc.i4.1
      IL_0002: stloc.0
      IL_0003: ldc.i4.0
      IL_0004: stloc.1
      IL_0005: ldc.i4.0
      IL_0006: stloc.2
      IL_0007: ldloc.0
      IL_0008: ldloc.1
      IL_0009: or
      IL_000a: stloc.3
      IL_000b: ldloc.3
      IL_000c: brfalse.s IL_0010
      IL_000e: ldc.i4.1
      IL_000f: stloc.2
      IL_0010: nop
      IL_0011: ret
      } // end of method Form1::foo
      <<-- OR

      -->> ANDALSO
      .method private instance void foo() cil managed
      {
      // Code size 25 (0x19)
      .maxstack 1
      .locals init ([0] bool a,
      [1] bool b,
      [2] bool c,
      [3] bool VB$CG$t_bool$S0)
      IL_0000: nop
      IL_0001: ldc.i4.1
      IL_0002: stloc.0
      IL_0003: ldc.i4.0
      IL_0004: stloc.1
      IL_0005: ldc.i4.0
      IL_0006: stloc.2
      IL_0007: ldloc.0
      IL_0008: brfalse.s IL_000d
      IL_000a: ldloc.1
      IL_000b: brtrue.s IL_0010
      IL_000d: ldc.i4.0
      IL_000e: br.s IL_0011
      IL_0010: ldc.i4.1
      IL_0011: stloc.3
      IL_0012: ldloc.3
      IL_0013: brfalse.s IL_0017
      IL_0015: ldc.i4.1
      IL_0016: stloc.2
      IL_0017: nop
      IL_0018: ret
      } // end of method Form1::foo

      <<-- ANDALSO

      -->> AND
      .method private instance void foo() cil managed
      {
      // Code size 18 (0x12)
      .maxstack 2
      .locals init ([0] bool a,
      [1] bool b,
      [2] bool c,
      [3] bool VB$CG$t_bool$S0)
      IL_0000: nop
      IL_0001: ldc.i4.1
      IL_0002: stloc.0
      IL_0003: ldc.i4.0
      IL_0004: stloc.1
      IL_0005: ldc.i4.0
      IL_0006: stloc.2
      IL_0007: ldloc.0
      IL_0008: ldloc.1
      IL_0009: and
      IL_000a: stloc.3
      IL_000b: ldloc.3
      IL_000c: brfalse.s IL_0010
      IL_000e: ldc.i4.1
      IL_000f: stloc.2
      IL_0010: nop
      IL_0011: ret
      } // end of method Form1::foo



      Nachtrag/Edit:
      Ich hab mal ein paar kleine Benchalgos geschrieben, die das ganze verdeutlichen.
      Die entscheidenden Zeilen (die jeweils 10 Mio. Mal aufgerufen werden):

      VB.NET-Quellcode

      1. 1 If (j Mod 100) = 0 And (j Mod 10000) = 0 Then
      2. 2 If (j Mod 100) = 0 AndAlso (j Mod 10000) = 0 Then
      3. 3 If (j Mod 10000) = 0 AndAlso (j Mod 100) = 0 Then
      4. 4 If CInt(Math.Sqrt(j)) Mod 10 = 0 And (j Mod 100) = 0 Then
      5. 5 If CInt(Math.Sqrt(j)) Mod 10 = 0 AndAlso (j Mod 100) = 0 Then
      6. 6 If (j Mod 100) = 0 AndAlso CInt(Math.Sqrt(j)) Mod 10 = 0 Then

      Die Zeiten:

      1: 0,670839276
      2: 0,375900016
      3: 0,366639636
      4: 1,664589456
      5: 1,367523228
      6: 0,380902164

      Man sieht dass bei den ersten drei (wobei beide Prüfungen verhältnissmäßig "billig" sind) das Konstrukt mit "And" deutlich langsamer ist, bei "AndAlso" aber kein SO großer Unterschied besteht.
      Bei den anderen drei ist "And" immer noch deutlich langsamer, aber der Hauptunterschied besteht zwischen den letzten beiden. Da hier die "teure" Bedingung (Berechnung der wurzel ist deutlich langsamer als ne Ganzahldivision mit Rest!), wenn sie als zweites geprüft wird, nicht mehr so oft berechnet werden muss, sind die Zeiten extrem unterschiedlich.

      Wie man sieht, kann man also durch die Verwendung von "AndAlso" und der richtigen Reihenfolge in Einzelfällen bis zu 3/4 Rechenzeit einsparen. Wenn das ganze nur einmal benötigt wird, ist das natürlich irrelevant, aber wenn das ganze in einer Schleife passiert, sollte man sich das mal überlegen!

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „picoflop“ ()

      Kompliment! Super erklärt, aber kleiner Fehler im Quelltext:
      Du erstellst den Stream "s", aber benutzt "a.Seek"

      Kannte diese Optimierung schon, aber die scheint nicht sehr bekannt zu sein. Gut, dass sie hier mal jemand aufzeigt!