none
Aus der App-Entwickler-Hotline: Per Reflection Methode aufrufen beschleunigen RRS feed

  • Allgemeine Diskussion

  • Hallo zusammen,
    heute wurde uns bei der App-Entwickler-Hotline unter anderem folgende Frage gestellt:

    Ich will eine Methode aufrufen, dessen Namen erst zur Laufzeit bekannt ist. Dazu verwende ich folgenden Code, der Reflection benutzt:

    public static class MethodInvokerReflection
    {
        public static object CallMethod(object obj, string methodName)
        {
            var m = obj.GetType().GetMethod(methodName);
            return m.Invoke(obj, new object[0]);
        }
    }
    Diese Methode wird allerdings sehr häufig aufgerufen. Da Reflection sehr langsam ist, lässt die Ausführungsgeschwindigkeit zu wünschen übrig. Wie kann ich den Code beschleunigen?

    Unsere Antwort bzw. unser Lösungsvorschlag darauf war:

    Eine Möglichkeit, den Code zu beschleunigen, besteht darin, zur Laufzeit CIL-Code zu erzeugen, welcher die Methode direkt aufruft. Wenn die Methode sehr häufig aufgerufen wird, fällt die Zeit, die zum Kompilieren benötigt wird, nicht mehr ins Gewicht, da der kompilierte Code zwischen den Methoden-Aufrufen wiederverwendet werden kann. Zum Zwischenspeichern gibt es mehrere Möglichkeiten, wovon wir zwei vorstellen wollen.

    Zum Einen kann ein "Dictionary" als Cache verwendet werden: Soll eine Methode auf einem Objekt aufgerufen werden, wird zunächst überprüft, ob im Cache ein Delegate mit dem Schlüssel (Klasse, Methoden-Name) existiert. Existiert dieser Delegate nicht, wird mittels "System.Linq.Expressions" ein neuer Delegate erzeugt, welcher das Ziel-Objekt entgegen nimmt und auf ihm die angegebene Methode ausführt. Dieser Delegate wird dann im Cache abgelegt. Anschließend wird er aufgerufen und das Ergebnis wird zurück gegeben.

    Dieser Ansatz ist etwa doppelt so schnell wie der Aufruf mittels Reflection. Wird die selbe Methode auf der selben Klasse mehrfach hintereinander aufgerufen, ist er sogar bis zu 6 Mal schneller.

    Der Code dazu sieht wie folgt aus:

    /// <summary>
    /// Diese Klasse erzeugt pro (Klasse, Methodenname)-Tupel einen
    /// neuen Delegate. Damit dieser Delegate nicht jedesmal neu kompiliert
    /// werden muss, wird er in einem Dictionary gecacht.
    /// Der kompilierte Delegate sieht etwa wie folgt aus:
    /// (object targetObject) => ((TargetClass)targetObject).Method();
    /// Damit hat "CallMethod" eine asymptotische Laufzeit von O(1).
    /// Wird zweimal von derselben Klasse die selbe Methode aufgerufen,
    /// greift ein zweiter Cache, welcher den Aufruf um 300% beschleunigt.
    /// </summary>
    public static class MethodInvokerCompiled1
    {
        /// <summary>
        /// Um die Performance zu erhöhen, werden die kompilierten Func-Instanzen gechached.
        /// </summary>
        private static Dictionary<string, Func<object, object>> compiledFuncs
            = new Dictionary<string, Func<object, object>>();
        /// <summary>
        /// Die jeweils zuletzt aufgerufene Methode wird noch einmal gecacht, 
        /// um ggf. das Dictionary zu umgehen.
        /// </summary>
        private static string lastMethodName;
        private static Type lastType;
        private static Func<object, object> lastCachedFunc;
    
        public static object CallMethod(object obj, string methodName)
        {
            var argType = obj.GetType();
    
            Func<object, object> func;
            if (lastMethodName == methodName && lastType == argType)
                func = lastCachedFunc;
            else
            {
                var key = argType.ToString() + "-" + methodName;
                if (!compiledFuncs.TryGetValue(key, out func))
                {
                    var argParameter = Expression.Parameter(typeof(object), "arg");
    
                    var lamdaExpression = Expression.Lambda(
                        Expression.GetFuncType(typeof(object), typeof(object)),
                        Expression.Call(Expression.Convert(argParameter, argType),
                            argType.GetMethod(methodName)), argParameter);
    
                    func = (Func<object, object>)lamdaExpression.Compile();
                    compiledFuncs[key] = func;
                    lastMethodName = methodName;
                    lastType = argType;
                    lastCachedFunc = func;
                }
            }
            return func(obj);
        }
    }

    Zum Anderen kann der erzeugte Delegate selbst als Cache genutzt werden: Wird er mit einem Ziel-Objekt und einem Methoden-Namen aufgerufen, prüft er in einem großen if-else-Konstrukt, ob die Klasse des Objekts und der Methoden-Name mit einem bekannten Eintrag übereinstimmt. Stimmt er überein, wird direkt die Methode aufgerufen. Stimmt er nicht überein, wird ein Fallback aufgerufen, in dem dieser bis dahin nicht berücksichtigte Fall in eine Liste aufgenommen und der Delegate unter Berücksichtigung dieser Liste neu kompiliert wird. Diese Methode ist auch bei mehreren unterschiedlichen Methoden-Aufrufen etwa 7 Mal schneller als der Ansatz mittels Reflection. Im Gegensatz zur ersten vorgestellten Methode skaliert dieser Ansatz aber nicht, da das if-else-Konstrukt mit jeder neuen Methode, die noch nicht berücksichtigt wurde, linear wächst.

    Der Code dazu sieht wie folgt aus:

    /// <summary>
    /// Diese Klasse verwendet einen Delegate, welcher neu kompiliert wird,
    /// sobald eine Methode zum ersten Mal aufgerufen wird.
    /// Da die Prüfungen, welche Methode aufgerufen werden soll, lineare if-Blöcke sind, 
    /// hat "CallMethod" eine asymptotische Laufzeit von O(n), wobei "n" die Anzahl
    /// der unterschiedlichen und bisher verwendeten (Klasse, Methodenname)-Tupel ist.
    /// 
    /// Der kompilierte Delegate sieht dann wie folgt aus:
    /// if (obj.GetType() == typeof(Foo) && methodName == "GetBar")
    ///     return ((Foo)obj).GetBar();
    /// else if (obj.GetType() == typeof(Foo2) && methodName == "GetBar")
    ///     return ((Foo2)obj).GetBar();
    /// [...]
    /// else return Fallback(obj, methodName); //Dies kompiliert den Delegate neu
    /// </summary>
    public static class MethodInvokerCompiled2
    {
        /// <summary>
        /// Um die Performance zu erhöhen, werden die kompilierten Func-Instanzen gechached.
        /// </summary>
        private static ICollection<Tuple<Type, MethodInfo>> addedFuncs
            = new List<Tuple<Type, MethodInfo>>();
        private static Func<object, string, object> compiledFunc;
    
        private static object Fallback(object obj, string methodName)
        {
            var argType = obj.GetType();
            addedFuncs.Add(Tuple.Create(argType, argType.GetMethod(methodName)));
    
            var fallbackMethodInfo = typeof(MethodInvokerCompiled2).GetMethod(
                "Fallback", BindingFlags.Static | BindingFlags.NonPublic);
            var getTypeMethodInfo = typeof(object).GetMethod("GetType");
    
            //parameters
            var targetObjParam = Expression.Parameter(typeof(object), "target");
            var methodNameParam = Expression.Parameter(typeof(string), "methodName");
    
            //variables
            var typeVariable = Expression.Variable(typeof(Type));
    
            //labels
            var returnTarget = Expression.Label(typeof(object));
    
            var setTypeExpression = Expression.Assign(typeVariable,
                Expression.Call(targetObjParam, getTypeMethodInfo));
    
            Expression body = Expression.Return(returnTarget,
                Expression.Call(fallbackMethodInfo, targetObjParam, methodNameParam));
    
            foreach (var f in addedFuncs)
            {
                var condition = Expression.AndAlso(
                    Expression.Equal(methodNameParam, Expression.Constant(f.Item2.Name)),
                    Expression.Equal(typeVariable, Expression.Constant(f.Item1)));
                var action = Expression.Return(returnTarget,
                    Expression.Call(Expression.Convert(targetObjParam, f.Item1), f.Item2));
    
                body = Expression.IfThenElse(condition, action, body);
            }
    
            var lamdaExpression = Expression.Lambda(
                Expression.GetFuncType(typeof(object), typeof(string), typeof(object)),
                Expression.Block(
                    new[] { typeVariable },
                    new[] 
                    {
                        setTypeExpression,
                        body, 
                        Expression.Label(returnTarget, Expression.Constant(null))
                    }
                ), targetObjParam, methodNameParam);
    
            compiledFunc = (Func<object, string, object>)lamdaExpression.Compile();
    
            return compiledFunc(obj, methodName);
        }
    
        public static object CallMethod(object obj, string methodName)
        {
            if (compiledFunc == null)
                return Fallback(obj, methodName);
            return compiledFunc(obj, methodName);
        }
    }

    Soll die Methode mit Argumenten aufgerufen werden, können diese auf ähnliche Weise wie der Methoden-Name berücksichtigt werden.

    Wir hoffen, vielen Besuchern der MSDN Foren durch das Posten dieses Problems und einer möglichen Lösung weiterhelfen zu können.

    Viele Grüße,
    Henning Dieterichs
    App-Entwickler-Hotline für MSDN Online Deutschland

    Disclaimer:
    Bitte haben Sie Verständnis dafür, dass wir hier auf Rückfragen gar nicht oder nur sehr zeitverzögert antworten können.
    Bitte nutzen Sie für Rückfragen oder neue Fragen den telefonischen Weg über die App-Entwickler-Hotline: http://www.msdn-online.de/Hotline
    App-Entwickler-Hotline: Schnelle & kompetente Hilfe für Entwickler: kostenfrei!

    Es gelten für die App-Entwickler-Hotline und dieses Posting diese Nutzungsbedingungen , Hinweise zu Markenzeichen, Informationen zur Datensicherheitsowie die gesonderten Nutzungsbedingungen für die App-Entwickler-Hotline.

    Mittwoch, 30. Juli 2014 14:39