Basic Benchmarks

    • Allgemein

      Basic Benchmarks

      Es gibt ja einige Varianten, wie man Daten in Klassen für von aussen zugreifbar machen kann:
      1. Public Feld
        Das bei weitem (!) schnellste. Allerdings hat man dabei keine Möglichkeit, Logik zu hinterlegen, die auf Änderungen irgendwie reagiert.
      2. Feld, gekapselt in Public Property
        Dieses eröffnet Möglichkeit, sowohl auf Setzen als auch auf Abruf zu reagieren. Ausserdem kann natürlich alles mögliche gekapselt sein, also ich muss nicht unbedingt ein Backing-Field anlegen, sondern kann sowohl den Setter als auch den Getter "durchleiten" auf andere Objekte. Etwa ein LogIn-Form kann eine Property Password haben, ohne ein Feld zu hinterlegen. Stattdessen leitet die Property Zugriffe einfach durch auf die dem Form evtl. aufsitzende PasswordTextbox.Text-Property.
        Auch kann die interne Logik jederzeit angepasst werden, ohne dass die Aussen-Sichtbarkeit überhaupt berührt wäre.
        Weiters unterstützen Properties Databinding
      3. Feld, gekapselt durch Methoden-Zugriff
        Prinzipiell das gleiche wie mit Properties. Nur sind bei Methoden nicht Getter und Setter zusammengefasst, sondern solch muss man jeweils extra implementieren. Ausserdem gilt die Konvention, dass Methoden auch komplexere Logik vollführen, während eine Property den Wert maximal schnell bereitstellen soll.
      4. Dependancy-Property
        Das sind besondere Properties, die in Wpf-Databinding-Szenarien allerlei erstaunliche Fähigkeiten zeigen: Änderungs-Überwachung, Auto-Korrektur, Validierung, Attachablität, ...
        Ein Merkmal ist auch, dass sie den FlyWeight-Pattern umsetzen. Also Objekte mit Dep-Properties belegen für diese Property nur Speicher, wenn auch ein individueller Wert zugewiesen ist.
      5. statisches Dictionary
        Eine Möglichkeit, eine FlyWeight-Property selbst zu implementieren ist, ein statisches Dictionary zu hinterlegen, wo Wertsetzungen und ihre Objekte gehalten werden - natürlich nur die Objekte, bei denen Werte auch gesetzt sind

      Diese 5 Typen haben natürlich sehr unterschiedliches Laufzeit verhalten.
      Etwa ein Feld abzurufen kann gar nicht direkt gemessen werden, denn die For-Schleife zum Generieren vieler Zugriffe ist selbst ca. vielfach langsamer das der Feld-Zugriff, der eiglich gemessen werden soll. Da müsste man eiglich iwelche Ausgleichsrechnungen anbringen, für die ich leider zu dumm (und zu faul) bin.
      Aber für alle anderen Arten lassen sich brauchbare Werte ermitteln.
      Hier meine zu testende Klasse Item, sie hat für jeden der o.g. Member-Typen einen Public Member
      Spoiler anzeigen

      VB.NET-Quellcode

      1. Class Item : Inherits DependencyObject
      2. Shared _dicItems As New Dictionary(Of Item, String)
      3. Public Field As String = "Huha"
      4. Private PrivateField As String = "PrivateField"
      5. Private Shared ReadOnly DepProperty As DependencyProperty = DependencyProperty.Register("DependencyProp", GetType(String), GetType(Item), New PropertyMetadata("DependencyPropDefault"))
      6. Public Property DependencyProp() As String
      7. Get
      8. Return DirectCast(GetValue(DepProperty), String)
      9. End Get
      10. Set(value As String)
      11. SetValue(DepProperty, value)
      12. End Set
      13. End Property
      14. Public Property Prop() As String
      15. Get
      16. Return PrivateField
      17. End Get
      18. Set(ByVal value As String)
      19. PrivateField = value
      20. End Set
      21. End Property
      22. Public Property DictionaryProp() As String
      23. Get
      24. Return _dicItems(Me)
      25. End Get
      26. Set(ByVal value As String)
      27. _dicItems(Me) = value
      28. End Set
      29. End Property
      30. Public Function FunctionProp() As String
      31. Return PrivateField
      32. End Function
      33. Public Function FunctionProp(value As String) As Item
      34. PrivateField = value
      35. Return Me
      36. End Function
      37. End Class


      MyStopWatch
      Ich stand vor dem Problem, einen gewaltigen Bereich unterschiedlicher Laufzeiten iwie vereinheitlichend zu testen - der schnellste Zugriff ist 100 mal schneller als der langsamste, also eine Mess-Schleife, die für den schnellsten Kandidaten 1s braucht, die auf den langsamsten anzuwenden fehlt mir die Geduld ;) . Auch wusste ich im voraus überhaupt nicht, wie die Testschleifen zu dimensionieren sind.
      Egal - meine MyStopWatch kehrt die Stopwatch-Logik um: Sie wird nicht gestartet, und muss warten, bis die Messung zuende ist, sondern sie gibt 1s vor, und zählt dabei, wie oft der Test durchlaufen wurde :D .
      Da diese Logik noch viel zeitintensiver ist als eine For-Schleife, und weil ich hier extrem schnelle Vorgänge teste, habich in meine MyStopWatch-Schleife noch ein For i = 0 To 10000 eingebastelt, das reduziert den Mystopwatch-Fehler auf vlt. 0,1%.
      Also meine Tests sehen vom Prinzip her alle so aus

      VB.NET-Quellcode

      1. While MyStopWatch.Until(1000, "Get Field")
      2. For i = 0 To 10000
      3. Dim x = itm.Field
      4. x = itm1.Field
      5. x = itm2.Field
      6. x = itm3.Field
      7. Next
      8. End While
      9. While MyStopWatch.Until(1000, "Set Field")
      10. For i = 0 To 10000
      11. itm.Field = "Hallo"
      12. itm1.Field = "Hallo"
      13. itm2.Field = "Hallo"
      14. itm3.Field = "Hallo"
      15. Next
      16. End While
      Ich greife in 4 verschiedenen Item auf den Member zu, mal als Getter, mal als Setter. Und das wie gesagt 10000 mal (ups! - 10001 mal ;) )

      Der Vollständigkeit halber auch MyStopWatch - aber eiglich nicht nötig, ist auch in den beiliegenden Sources:
      Spoiler anzeigen

      VB.NET-Quellcode

      1. Public Class MyStopWatch
      2. 'Use: enclose what you want to benchmark in a While MyStopWatch.Until() - loop. It will display, how
      3. '! often the enclosed stuff was executed
      4. Private Sub UntilDemo()
      5. While MyStopWatch.Until(1000, "Demo")
      6. Console.WriteLine("Demo, Text" & Date.Now)
      7. End While
      8. End Sub
      9. Private Shared _Counter As Integer
      10. Private Shared _LastTick As Integer = Integer.MinValue
      11. Public Shared Function Until( _
      12. RunTime As Integer, Optional Msg As String = "MyStopWatch") As Boolean
      13. If Environment.TickCount < _LastTick Then
      14. _Counter += 1
      15. ElseIf _LastTick = Integer.MinValue Then
      16. _LastTick = Environment.TickCount + RunTime
      17. _Counter = 0
      18. Else
      19. Debug.WriteLine(String.Format("{0,25}: {1,10}", Msg, _Counter))
      20. _LastTick = Integer.MinValue
      21. Return False
      22. End If
      23. Return True
      24. End Function
      25. End Class
      Jedenfalls durch die umgedrehte Logik bedeuten bei meinen Messungen große Kennzahlen Schnelligkeit, und kleine Lahmheit.
      Hier mal die erste Runde:

      Quellcode

      1. BasicAccesses
      2. Leerlauf: 17676
      3. Get Field: 15877
      4. Set Field: 5432
      5. Get Prop: 3567
      6. Set Prop: 2789
      7. Get Function: 3329
      8. Set Function: 2562
      9. Get DictionaryProp: 470
      10. Set DictionaryProp: 437
      11. Get DependencyProp: 301
      12. Set DependencyProp: 91
      Wie man sieht, bei Get Field ist der Leerlauf, der den "Eigenverbrauch" der Mess-Schleife anzeigt, nur 10% schneller. Also bereinigt läge der Get Field - Wert womöglich 10 mal höher.
      Aber schon bei Set Field dreht sich das um - hier ist der Leerlauf ca. 300% schneller, fällt also ca. zu 1/3 ins Gewicht - bereinigt käme also evtl. 7000 bei raus. (Ich wäre wirklich dankbar, wenn ein Mathe-Crack da mal die richtigen Formeln drauf anwendet).
      Properties und Methoden stehen ungefähr gleich, aber der FlyWeight-Zugriff über ein Dictionary geht natürlich schwer in' Keller. Also eiglich erstaunlich, wie schnell ein Dic ist, weil zu jedem Zugriff ist ja einiges an Algorithmus zu absolvieren.
      Schwer überrascht hat mich die miese Performance von Dependancy-Properties - nicht halb so schnell wie ein Dictionary!
      Aber insgesamt muss man sehen: Im Einzelfall ist das alles ziemlich wurst.
      Die Werte sind ja mit 400000 zu multiplizieren, um die Anzahl der Zugriffe/s zu erhalten, also so oder so ist das hinreichend flott.

      Generische Zugriffe - Delegaten
      Das hat mich eigentlich interessiert. Es gibt ja ein paar Möglichkeiten, ohne konkrete Kenntnis einer Klasse dennoch auf deren Member zuzugreifen.
      Königsweg sind da Delegaten - denen kann ich ja eine anonyme Methode verpassen, sodass, wer das aufruft, nicht mehr weiß, was er eigentlich aufruft:

      VB.NET-Quellcode

      1. Dim getter As Func(Of Item, String) = Function(itm) itm.Field
      2. While MyStopWatch.Until(1000, "Get Field")
      3. For i = 0 To 10000
      4. Dim x = getter(itm)
      5. x = getter(itm1)
      6. x = getter(itm2)
      7. x = getter(itm3)
      8. Next
      9. End While
      10. getter = Function(itm) itm.Prop
      11. While MyStopWatch.Until(1000, "Get Prop")
      12. For i = 0 To 10000
      13. Dim x = getter(itm)
      14. x = getter(itm1)
      15. x = getter(itm2)
      16. x = getter(itm3)
      17. Next
      18. End While
      Wie man sieht: dieselbe Delegat-Variable getter ruft mal das Feld ab, mal die Property. Und die Performance ist durchaus noch durchaus ansehnlich:

      Quellcode

      1. TestDelegates
      2. Get Field: 3180
      3. Set Field: 2444
      4. Get Prop: 1937
      5. Set Prop: 1702


      Reflection
      Für andere Könige hingegen mag Reflection der Königsweg sein. Weil um den Delegaten bauen zu können muss man den Item-Datentyp doch kennen. Also der Zugriff via Delegat braucht ihn nicht zu kennen, aber wo der Delegat gebaut wird (zeilen#1, #10), da muss der Item.Field / Item.Prop-Member bekannt sein.
      Reflection braucht das nicht:

      VB.NET-Quellcode

      1. Dim tp As Type = anObject.GetType()
      2. tp = GetType(Item) 'alternativ
      3. Dim pInf As PropertyInfo = tp.GetProperty("Prop")
      4. Dim fieldInf As FieldInfo = tp.GetField("Field")
      tp ist ein Type, und solch kann man von jedem Objekt mittels GetType()-Funktion abrufen, aber auch von beliebigem Datentyp, mittels GetType()-Schlüsselwort.
      Ist natürlcih die höchste Unsicherheitsstufe, weil wenn der Datentyp den gesuchten Member nicht aufweist, passiert garnix. (Später passiert dann was, wenn man das Field-/Property-Info dann benutzen will ;) )
      Mit Reflection können sogar Private Member zugegriffen werden, also das ermöglicht auch Sachen, die von der OOP-Sprache her eigentlich nicht möglich sein sollten.
      Das traurige an diesem "Königsweg" ist aber die Performance:

      Quellcode

      1. TestReflection
      2. Get Field: 149
      3. Set Field: 106
      4. Get Function: 93
      5. Set Function: 52
      6. Get Prop: 89
      7. Set Prop: 53
      also zw. 30-50 mal langsamer als wenn man standard-mäßig und typisiert zugreift. Ist also eher was für langsamere Könige, dieser Weg.

      MemberAccess
      Weil ich nun aber grad das brauche: einen Member-Zugriff, einfach per String zu definieren - ohne Kenntnis des Datentyps - habich nochmal die Linq.Expression-Trickkiste durchgewühlt, und fand tatsächlich Mittel, um Field-/Property-Infos in Lambda-ExpressionTrees einzubauen, die sich zu Delegaten kompilieren lassen.
      MemberAccess

      VB.NET-Quellcode

      1. Imports System.Collections.Generic
      2. Imports System.Linq.Expressions
      3. Imports System.Reflection
      4. ''' <summary> encapsulates 2 Delegates for read/write access a member of TOwner. Either set them manually or let Reflection derive them from Member-Name. In latter case also private Members are accessible, and the accesses are upto 30 times faster than original reflection </summary>
      5. Public Class MemberAccess(Of TOwner, TMember)
      6. Public ReadOnly Getter As Func(Of TOwner, TMember)
      7. Public ReadOnly Setter As Action(Of TOwner, TMember)
      8. Public ReadOnly Name As String
      9. Public Sub New(getter As Func(Of TOwner, TMember), Optional setter As Action(Of TOwner, TMember) = Nothing, Optional memberName As String = Nothing)
      10. Me.Getter = getter
      11. Me.Setter = setter
      12. Me.Name = memberName
      13. End Sub
      14. ''' <summary> note: Instead of Exception on readonly-members the Setter-Initialization will be omitted </summary>
      15. Public Sub New(memberName As String, Optional friendlyName As String = Nothing)
      16. Me.Name = If(friendlyName, memberName)
      17. Dim tpOwner = GetType(TOwner)
      18. Dim flags = BindingFlags.Instance Or BindingFlags.[Public] Or BindingFlags.NonPublic
      19. Dim mmb As MemberInfo = tpOwner.GetProperty(memberName, flags)
      20. If mmb Is Nothing Then mmb = tpOwner.GetField(memberName, flags)
      21. If mmb Is Nothing Then _
      22. Throw New KeyNotFoundException(String.Format("member '{0}.{1}' not found", tpOwner.Name, memberName))
      23. Dim ownerParamX As ParameterExpression = Expression.Parameter(tpOwner, "owner")
      24. Dim memberParamX As ParameterExpression = Expression.Parameter(GetType(TMember), "value")
      25. Dim mmbX As MemberExpression = Expression.MakeMemberAccess(ownerParamX, mmb)
      26. Getter = Expression.Lambda(Of Func(Of TOwner, TMember))(mmbX, "get" & memberName, True, {ownerParamX}).Compile()
      27. Try
      28. Dim assignX = Expression.Assign(mmbX, memberParamX)
      29. Setter = Expression.Lambda(Of Action(Of TOwner, TMember))(assignX, "set" & memberName, True, {ownerParamX, memberParamX}).Compile()
      30. Catch
      31. End Try
      32. End Sub
      33. Default Public Property Item(owner As TOwner) As TMember
      34. Get
      35. Return Getter(owner)
      36. End Get
      37. Set(value As TMember)
      38. Setter(owner, value)
      39. End Set
      40. End Property
      41. End Class
      Ich speichere auch den Namen des Members, dadurch zieht die Klasse ziemlich gleich mit dem, was Reflection-MemberInfo bereitstellt. Auch kann man die Delegaten direkt angeben, ohne den Reflection-Hack zu nutzen. Brauche ich in meim aktuellen Projekt so, dann hab ichs einheitlich, vorzugsweise ohne Reflection, aber zur Not dann eben halt mit.
      Ausserdem gibts eine Default-Property, die den Werte-Abruf bischen vereinfacht.

      VB.NET-Quellcode

      1. ' Statt
      2. dim value = myAccess.Getter(item)
      3. 'und
      4. myAccess.Setter(item, newValue)
      5. 'heisstes
      6. dim value = myAccess(item)
      7. myAccess(item) = newValue


      Damit hab ich also die Flexiblität von Reflection mit der Geschwindigkeit von Delegaten kombiniert:

      Quellcode

      1. TestMemberAccess
      2. Field-Getter: 4314
      3. Field-Setter: 3094
      4. PropGetter: 838
      5. PropSetter: 772
      (Einzig wundert mich, dass dieser Ansatz bei Feldern sogar schneller ist als der Delegat-Ansatz, bei Properties hingegen langsamer)

      Hier nochmal alle Tests in GesamtSchau
      alle Tests

      Quellcode

      1. Leerlauf: 17676
      2. TestPublicMembers
      3. Get Field: 15877
      4. Set Field: 5432
      5. Get Prop: 3567
      6. Set Prop: 2789
      7. Get Function: 3329
      8. Set Function: 2562
      9. Get DictionaryProp: 470
      10. Set DictionaryProp: 437
      11. Get DependencyProp: 301
      12. Set DependencyProp: 91
      13. TestDelegates
      14. Get Field: 3180
      15. Set Field: 2444
      16. Get Prop: 1937
      17. Set Prop: 1702
      18. TestMemberAccess
      19. Field-Getter direct: 4314
      20. Field-Setter direct: 3094
      21. PropGetter direct: 838
      22. PropSetter direct: 772
      23. TestReflection
      24. Get Field: 149
      25. Set Field: 106
      26. Get Function: 93
      27. Set Function: 52
      28. Get Prop: 89
      29. Set Prop: 53


      Zusammenfassung
      • Zunächstmal haben wir einen flüchtigen Blick auf Kapselungs-Varianten geworfen, sei es ungekapselt, durch Property oder durch Methode. Auch was gekapselt ist, ist interessant: ein Feld, die Property eines anderen, komplexen Objekts, oder auch ein statisches Dictionary (Flyweight-Pattern)
      • Dann wurde MyStopwatch vorgestellt, mit der man besonders komfortabel aussagekräftige Benchmarks erstellen kann, mit Besonderheit der umgedrehte Zeitmessungs-Logik.
      • Dann gibts einige Test-Ergebnisse, vlt. nicht ganz uninteressant
      • Dann kam ich aufs Problem generischen Zugriffes zu sprechen, also dass man manchmal von einem Objekt Werte abrufen möchte, ohne dass dessen Datentyp von vornherein festgelegt ist. Hierbei wurden 2 Möglichkeiten betrachtet, zum einen Delegaten - die müssen nicht mehr das Objekt kennen, sondern nur noch den Datentyp des zuzugreifenden Wertes.
        Hingegen Reflection ist gar nicht mehr typsicher - hier kann von jedem Objekt alles zugegriffen werden, sogar private (eigentlich gekapselte) Member. Reflection verstößt damit klar gegen das Typisierungs-Prinzip der Sprache, und ist eine äusserst unsichere Vorgehensweise. Ausserdem unkomfortabel und langsam.
      • Zuguterletzt wurde noch die MemberAccess-Klasse eingeführt, die Reflection-Zugriffe vorkompiliert und als Delegat speichert. Ist natürlich ebenso unsicher, aber auch so mächtig wie Reflection (kann auch Kapselung unterlaufen), aber v.a. bis zu 30mal schneller.
      Anzumerken noch, dass man sich mit Performance-Fragen auf keinen Fall verrückt machen soll. Fast immer ist eine saubere Architektur wesentlich ausschlaggebender. Solch macht Code nicht nur verständlicher, sondern auch schneller. Weil eine durchdachte Anordnung von Zuständigkeiten ermöglicht es erst, dass man erforderlichenfalls für einen kleinen problematischen Bereich besondere Optimierungen codet - was sich dann für die ganze Anwendung gewinnbringend auswirkt.
      Dateien

      Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „ErfinderDesRades“ ()