C# Performance bei der Befüllung/Mapping von Massendaten beschleunigen.

7. Mai 2012

Beschleunigung von PropertyInfo.SetValue bzw. PropertyInfo.GetValue

Die Methode PropertyInfo.SetValue bzw. PropertyInfo.GetValue aus dem Namespace Reflection ermöglicht den Zugriff auf Objekt-Properties zur Laufzeit. Genutzt werden die Methoden beispielsweise bei jeglicher Form von Daten-Mapping da Werte von einem Objekt-Typ in einen anderen Objekt-Typ kopiert werden.

// entspricht myObject.MyProperty = myValue 

PropertyInfo propertyInfo = …

propertyInfo.SetValue(myObject, myValue, null);

Beide Methoden sind mächtig und bequem – aber extrem langsam. Es geht aber auch deutlich schneller. Mit einem kleinen Helper (siehe Ende des Artikels) kann man durchaus den 10-fachen Durchsatz erreichen. Anbei ein paar Vergleichszeiten die bei der Befüllung von 5Mio Objekten mit 20 Properties ermittelt wurden

  * Properties direkt schreiben (Referenz)

3.4 s

  * Properties per PropertyInfo.SetValue befüllen

129.9 s

  * Properties per Helper (TypedSetter)

4.0 s

  * Properties per Helper (UnTypedSetter)

9.8 s

Wie langsam ist SetValue per Reflection ?

In fast allen Softwareprojekten gibt es Methoden, in denen Massen von Objekten durch eine Datenquelle initialisiert und befüllt werden. Dabei werden in einfachen Fällen die Properties der Objekte von Hand gesetzt.

// Properties der Objekte werden direkt zugewiesen

while (reader.Read())

{

    MyClass myObject = new MyClass();

    myObject.IntValue = reader.GetInt32(fieldNumberInt);

    myObject.StringValue = reader.GetString(fieldNumberString);

}

Sollen beliebige Objekttypen unterstützt werden, so wird Reflection eingesetzt um ein generisches Befüllen der Objekte vorzunehmen. Dabei ist es wichtig, alle PropertyInfos eines Typen einmalig per Reflection vorab zu ermitteln und zu hinterlegen. Ansonsten dauert die Ermittlung der Typinformation länger als das eigentliche Setzen des Wertes.

// Feldnamen und Properties ausserhalb der Schleife ermitteln

List<string> fieldNames = GetFieldNames(reader);

List<PropertyInfo> propertyInfoList = GetPropertyInfos(typeof(MyClass));

while (reader.Read())

{

    //Objekt per Reflection erzeugen

    MyClass o = Activator.CreateInstance(typeof (MyClass)) as MyClass;

    int fieldNumber = 0;

    foreach (PropertyInfo propertyInfo in propertyInfoList)

    {

        // Annahme: Felder und Properties sind in Anzahl und Reihenfolge gleich

        object val = reader.GetValue(fieldNumber);

        propertyInfo.SetValue(o, val, null);

        fieldNumber++;

    }

    result.Add(o);

}

Vergleicht man die Laufzeitunterschiede zum Befüllen von 5Mio Objekten einer Testklasse mit 20 Properties, so ergeben sich folgende Laufzeiten (Nettozeit der Zuweisungsschleife ohne die Ermittlung der PropertyInfos):

Property direkt zuweisen: 3.4 sec

Property per SetValue: 129.9 sec

Die Implementierung per Reflection hat somit die 35fache Zeit benötigt – je nach Rechner wird ein Faktor von 15 – 40 ermittelt.

Nach meiner persönlichen Daumenregel wird bei interaktiven Anwendungen der Laufzeitunterschied ab einer Größenordnung von 100.000 Objekten relevant. Die längeren Laufzeiten zur Befüllung per SetValue erreichen dann den Sekundenbereich und werden so durch den Benutzer als störend wahrgenommen.

Schneller Zugriff

Schneller funktioniert der Zugriff mit einem „Methoden-Pointer“ auf den Setter. In C# ist dies ein Delegate der direkt die SetMethode abgreift.

MyClass myObject = new MyClass();

PropertyInfo propertyInfo = myObject.GetType().GetProperty("IntProperty");

Action<MyClass, int> reflSet = (Action<MyClass, int>)Delegate.CreateDelegate(

    typeof(Action<MyClass, int>), propertyInfo.GetSetMethod());

// Schneller Zugriff auf das Property

reflSet.Invoke(myObject, 10);

Die Zugriffsgeschwindigkeit entspricht dabei dem direkten Zugriff auf die Properties. Allerdings wird dadurch noch kein exakter Ersatz für die Invoke-Methode bereitgestellt, da der jeweilige Typ in der Action-Deklaration angegeben werden muss

Action<MyClass, int>.

Benötigt wird somit ein Delegate, der eine „nach Object gecastete“ Action bereitstellt. Der Zusammenbau aus Cast und Setter-Methode wird über Expressions bereitgestellt und ist dementsprechend ein bisschen komplexer.

Erzeugt wird somit eine Action<T, object> bei der das object in den Typ des Zielproperties konvertiert wird. Klingt kompliziert? Ist es auch (ich habe doch recht lange dafür gebraucht ;), obwohl die Implementierung recht kurz ist.

var targetType = propertyInfo.DeclaringType;

var methodInfo = propertyInfo.GetSetMethod();

var exTarget = Expression.Parameter(targetType, "t");

var exValue = Expression.Parameter(typeof(object), "p");

 

// anObject.SetPropertyValue(object) mit Convert in den Typ des Properties

var exBody = Expression.Call(exTarget, methodInfo,

    Expression.Convert(exValue, propertyInfo.PropertyType));

var lambda = Expression.Lambda<Action<T, object>>(exBody, exTarget, exValue);

 

// (t, p) => t.set_StringValue(Convert(p))

var action = lambda.Compile();

 

Der Aufruf sieht jetzt aus wie vorher. Der Parameter ist jetzt jedoch vom Typ object.

action.invoke(myObject, 10)

FastInvoke Helper – Ein Beispiel

Eine kleine Helper-Klasse liefert die gewünschten “Methoden-Pointer” auf die Properties.

namespace Sdx.Malsy.FastInvoke

{

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using System.Reflection;

    using System.Linq.Expressions;

 

    /// <summary>

    /// Bei Gefallen würde es mich freuen davon zu hören. Schreibt eine Mail an "matthias.malsy" an die "sdx-ag.de".

    /// </summary>

    public class FastInvoke

    {

        public static Func<T, TReturn> BuildTypedGetter<T, TReturn>(PropertyInfo propertyInfo)

        {

            Func<T, TReturn> reflGet = (Func<T, TReturn>)

                Delegate.CreateDelegate(typeof(Func<T, TReturn>), propertyInfo.GetGetMethod());

            return reflGet;

        }

 

        public static Action<T, TProperty> BuildTypedSetter<T, TProperty>(PropertyInfo propertyInfo)

        {

            Action<T, TProperty> reflSet = (Action<T, TProperty>)Delegate.CreateDelegate(

                typeof(Action<T, TProperty>), propertyInfo.GetSetMethod());

            return reflSet;

        }

 

        public static Action<T, object> BuildUntypedSetter<T>(PropertyInfo propertyInfo)

        {

            var targetType = propertyInfo.DeclaringType;

            var methodInfo = propertyInfo.GetSetMethod();

            var exTarget = Expression.Parameter(targetType, "t");

            var exValue = Expression.Parameter(typeof(object), "p");

            // wir betreiben ein anObject.SetPropertyValue(object)

            var exBody = Expression.Call(exTarget, methodInfo,

                                       Expression.Convert(exValue, propertyInfo.PropertyType));

            var lambda = Expression.Lambda<Action<T, object>>(exBody, exTarget, exValue);

            // (t, p) => t.set_StringValue(Convert(p))

 

            var action = lambda.Compile();

            return action;

        }

 

        public static Func<T, object> BuildUntypedGetter<T>(PropertyInfo propertyInfo)

        {

            var targetType = propertyInfo.DeclaringType;

            var methodInfo = propertyInfo.GetGetMethod();

            var returnType = methodInfo.ReturnType;

 

            var exTarget = Expression.Parameter(targetType, "t");

            var exBody = Expression.Call(exTarget, methodInfo);

            var exBody2 = Expression.Convert(exBody, typeof(object));

 

            var lambda = Expression.Lambda<Func<T, object>>(exBody2, exTarget);

            // t => Convert(t.get_Foo())

 

            var action = lambda.Compile();

            return action;

        }

    }

}

Im nachfolgenden Bespiel werden Daten aus einem DataReader gelesen und für jeden Datensatz ein Objekt des Template-Typs T erzeugt.

// Erzeugt und befüllt Objekte sehr schnell aus einem DataReader

static List<T> CreateObjectFromReader<T>(IDataReader reader)

    where T : new()

{

  // Vorbeitung

  List<string> fieldNames = GetFieldNames(reader);

  List<Action<T, object>> setterList = new List<Action<T, object>>();

 

  // Aus den Feldnamen des Readers die "Property-Setter" erzeugen

  // und in einem Array merken

  foreach (var field in fieldNames)

  {

    var propertyInfo = typeof(T).GetProperty(field);

    setterList.Add(FastInvoke.BuildUntypedSetter<T>(propertyInfo));

  }

  Action<T, object>[] setterArray = setterList.ToArray();

 

  // Objekte in einer Schleife erzeugen

  while (reader.Read())

  {

    T xclass = new T();

    int fieldNumber = 0;

 

    for (int i = 0; i< setterArray.Length; i++) 

    {

        setterArray[i](xclass, reader.GetValue(i));

        fieldNumber++;

    } 

    result.Add(xclass);

  }

}

Fazit

Egal ob Massendaten per Reflection gelesen oder geschrieben werden, der Zugriff über PropertyInfo.Get oder PropertyInfo.Set ist deutlich zu langsam. Mit dem FastInvoke-Helper holt man sich direkt den “Methoden-Pointer” auf die Properties und kann fast ohne Geschwindigkeitsverlust den Zugriff durchführen.

Wer den Mapper nicht selber schreiben möchte kann "AutoMapper – The object-object mapper" nutzen.

Die Sourcen des Artikels inklusive Geschwindigkeitsmessung können hier heruntergeladen werden.