Es gibt ja einige Varianten, wie man Daten in Klassen für von aussen zugreifbar machen kann:
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
Spoiler anzeigen
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
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
Also meine Tests sehen vom Prinzip her alle so aus
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
Spoiler anzeigen
Jedenfalls durch die umgedrehte Logik bedeuten bei meinen Messungen große Kennzahlen Schnelligkeit, und kleine Lahmheit.
Hier mal die erste Runde:
Wie man sieht, bei
Aber schon bei
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:
Wie man sieht: dieselbe Delegat-Variable
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
Reflection braucht das nicht:
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
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:
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
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.
Damit hab ich also die Flexiblität von Reflection mit der Geschwindigkeit von Delegaten kombiniert:(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
Zusammenfassung
- Public Feld
Das bei weitem (!) schnellste. Allerdings hat man dabei keine Möglichkeit, Logik zu hinterlegen, die auf Änderungen irgendwie reagiert. - 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 PropertyPassword
haben, ohne ein Feld zu hinterlegen. Stattdessen leitet die Property Zugriffe einfach durch auf die dem Form evtl. aufsitzendePasswordTextbox.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 - 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. - 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. - 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 MemberVB.NET-Quellcode
- Class Item : Inherits DependencyObject
- Shared _dicItems As New Dictionary(Of Item, String)
- Public Field As String = "Huha"
- Private PrivateField As String = "PrivateField"
- Private Shared ReadOnly DepProperty As DependencyProperty = DependencyProperty.Register("DependencyProp", GetType(String), GetType(Item), New PropertyMetadata("DependencyPropDefault"))
- Public Property DependencyProp() As String
- Get
- Return DirectCast(GetValue(DepProperty), String)
- End Get
- Set(value As String)
- SetValue(DepProperty, value)
- End Set
- End Property
- Public Property Prop() As String
- Get
- Return PrivateField
- End Get
- Set(ByVal value As String)
- PrivateField = value
- End Set
- End Property
- Public Property DictionaryProp() As String
- Get
- Return _dicItems(Me)
- End Get
- Set(ByVal value As String)
- _dicItems(Me) = value
- End Set
- End Property
- Public Function FunctionProp() As String
- Return PrivateField
- End Function
- Public Function FunctionProp(value As String) As Item
- PrivateField = value
- Return Me
- End Function
- 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 .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
Der Vollständigkeit halber auch
MyStopWatch
- aber eiglich nicht nötig, ist auch in den beiliegenden Sources:
VB.NET-Quellcode
- Public Class MyStopWatch
- 'Use: enclose what you want to benchmark in a While MyStopWatch.Until() - loop. It will display, how
- '! often the enclosed stuff was executed
- Private Sub UntilDemo()
- While MyStopWatch.Until(1000, "Demo")
- Console.WriteLine("Demo, Text" & Date.Now)
- End While
- End Sub
- Private Shared _Counter As Integer
- Private Shared _LastTick As Integer = Integer.MinValue
- Public Shared Function Until( _
- RunTime As Integer, Optional Msg As String = "MyStopWatch") As Boolean
- If Environment.TickCount < _LastTick Then
- _Counter += 1
- ElseIf _LastTick = Integer.MinValue Then
- _LastTick = Environment.TickCount + RunTime
- _Counter = 0
- Else
- Debug.WriteLine(String.Format("{0,25}: {1,10}", Msg, _Counter))
- _LastTick = Integer.MinValue
- Return False
- End If
- Return True
- End Function
- End Class
Hier mal die erste Runde:
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
- Dim getter As Func(Of Item, String) = Function(itm) itm.Field
- While MyStopWatch.Until(1000, "Get Field")
- For i = 0 To 10000
- Dim x = getter(itm)
- x = getter(itm1)
- x = getter(itm2)
- x = getter(itm3)
- Next
- End While
- getter = Function(itm) itm.Prop
- While MyStopWatch.Until(1000, "Get Prop")
- For i = 0 To 10000
- Dim x = getter(itm)
- x = getter(itm1)
- x = getter(itm2)
- x = getter(itm3)
- Next
- End While
getter
ruft mal das Feld ab, mal die Property. Und die Performance ist durchaus noch durchaus ansehnlich: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:
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:
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.
VB.NET-Quellcode
- Imports System.Collections.Generic
- Imports System.Linq.Expressions
- Imports System.Reflection
- ''' <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>
- Public Class MemberAccess(Of TOwner, TMember)
- Public ReadOnly Getter As Func(Of TOwner, TMember)
- Public ReadOnly Setter As Action(Of TOwner, TMember)
- Public ReadOnly Name As String
- Public Sub New(getter As Func(Of TOwner, TMember), Optional setter As Action(Of TOwner, TMember) = Nothing, Optional memberName As String = Nothing)
- Me.Getter = getter
- Me.Setter = setter
- Me.Name = memberName
- End Sub
- ''' <summary> note: Instead of Exception on readonly-members the Setter-Initialization will be omitted </summary>
- Public Sub New(memberName As String, Optional friendlyName As String = Nothing)
- Me.Name = If(friendlyName, memberName)
- Dim tpOwner = GetType(TOwner)
- Dim flags = BindingFlags.Instance Or BindingFlags.[Public] Or BindingFlags.NonPublic
- Dim mmb As MemberInfo = tpOwner.GetProperty(memberName, flags)
- If mmb Is Nothing Then mmb = tpOwner.GetField(memberName, flags)
- If mmb Is Nothing Then _
- Throw New KeyNotFoundException(String.Format("member '{0}.{1}' not found", tpOwner.Name, memberName))
- Dim ownerParamX As ParameterExpression = Expression.Parameter(tpOwner, "owner")
- Dim memberParamX As ParameterExpression = Expression.Parameter(GetType(TMember), "value")
- Dim mmbX As MemberExpression = Expression.MakeMemberAccess(ownerParamX, mmb)
- Getter = Expression.Lambda(Of Func(Of TOwner, TMember))(mmbX, "get" & memberName, True, {ownerParamX}).Compile()
- Try
- Dim assignX = Expression.Assign(mmbX, memberParamX)
- Setter = Expression.Lambda(Of Action(Of TOwner, TMember))(assignX, "set" & memberName, True, {ownerParamX, memberParamX}).Compile()
- Catch
- End Try
- End Sub
- Default Public Property Item(owner As TOwner) As TMember
- Get
- Return Getter(owner)
- End Get
- Set(value As TMember)
- Setter(owner, value)
- End Set
- End Property
- End Class
Ausserdem gibts eine Default-Property, die den Werte-Abruf bischen vereinfacht.
Damit hab ich also die Flexiblität von Reflection mit der Geschwindigkeit von Delegaten kombiniert:(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
Quellcode
- Leerlauf: 17676
- TestPublicMembers
- Get Field: 15877
- Set Field: 5432
- Get Prop: 3567
- Set Prop: 2789
- Get Function: 3329
- Set Function: 2562
- Get DictionaryProp: 470
- Set DictionaryProp: 437
- Get DependencyProp: 301
- Set DependencyProp: 91
- TestDelegates
- Get Field: 3180
- Set Field: 2444
- Get Prop: 1937
- Set Prop: 1702
- TestMemberAccess
- Field-Getter direct: 4314
- Field-Setter direct: 3094
- PropGetter direct: 838
- PropSetter direct: 772
- TestReflection
- Get Field: 149
- Set Field: 106
- Get Function: 93
- Set Function: 52
- Get Prop: 89
- 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.
Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „ErfinderDesRades“ ()