Considérez la manipulation simple suivante sur une collection:
static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);
Utilisons maintenant des expressions. Le code suivant est à peu près équivalent:
static void UsingLambda() {
Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambda(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda: {0}", tn - t0);
}
Mais je veux créer l'expression à la volée, voici donc un nouveau test:
static void UsingCompiledExpression() {
var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);
var c3 = f.Compile();
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = c3(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}
Bien sûr, ce n'est pas exactement comme ce qui précède, donc pour être honnête, je modifie légèrement le premier:
static void UsingLambdaCombined() {
Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambdaCombined(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda combined: {0}", tn - t0);
}
Viennent maintenant les résultats pour MAX = 100000, VS2008, débogage ON:
Using lambda compiled: 23437500
Using lambda: 1250000
Using lambda combined: 1406250
Et avec le débogage désactivé:
Using lambda compiled: 21718750
Using lambda: 937500
Using lambda combined: 1093750
Surprise . L'expression compilée est environ 17 fois plus lente que les autres alternatives. Maintenant, voici les questions:
- Est-ce que je compare des expressions non équivalentes?
- Existe-t-il un mécanisme pour que .NET «optimise» l'expression compilée?
- Comment exprimer le même appel en chaîne par
l.Where(i => i % 2 == 0).Where(i => i > 5);
programme?
Quelques statistiques supplémentaires. Visual Studio 2010, débogage activé, optimisations désactivées:
Using lambda: 1093974
Using lambda compiled: 15315636
Using lambda combined: 781410
Débogage ON, optimisations ON:
Using lambda: 781305
Using lambda compiled: 15469839
Using lambda combined: 468783
Débogage OFF, optimisations ON:
Using lambda: 625020
Using lambda compiled: 14687970
Using lambda combined: 468765
Nouvelle surprise. Le passage de VS2008 (C # 3) à VS2010 (C # 4), rend le UsingLambdaCombined
plus rapide que le lambda natif.
Ok, j'ai trouvé un moyen d'améliorer les performances compilées lambda de plus d'un ordre de grandeur. Voici une astuce; après l'exécution du profileur, 92% du temps est consacré à:
System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
Hmmmm ... Pourquoi crée-t-il un nouveau délégué à chaque itération? Je ne suis pas sûr, mais la solution suit dans un article séparé.
la source
Stopwatch
pour les horaires plutôt que pourDateTime.Now
.Réponses:
Se pourrait-il que les lambdas internes ne soient pas compilés?!? Voici une preuve de concept:
Et maintenant, les horaires sont:
Woot! Non seulement il est rapide, mais il est plus rapide que le lambda natif. ( Tête à gratter ).
Bien sûr, le code ci-dessus est tout simplement trop pénible à écrire. Faisons de la magie simple:
Et quelques timings, VS2010, optimisations ON, débogage OFF:
Vous pouvez maintenant affirmer que je ne génère pas l'expression entière de manière dynamique; juste les invocations de chaînage. Mais dans l'exemple ci-dessus, je génère l'expression entière. Et les horaires correspondent. Ceci est juste un raccourci pour écrire moins de code.
D'après ce que j'ai compris, ce qui se passe, c'est que la méthode .Compile () ne propage pas les compilations aux lambdas internes, et donc l'invocation constante de
CreateDelegate
. Mais pour vraiment comprendre cela, je serais ravi d'avoir un commentaire d'un gourou .NET sur les choses internes en cours.Et pourquoi , oh pourquoi est-ce maintenant plus rapide qu'un lambda natif !?
la source
Récemment, j'ai posé une question presque identique:
Performances de l'expression compilée pour déléguer
La solution pour moi était que je ne devais pas appeler
Compile
leExpression
, mais que je devais l'appelerCompileToMethod
et le compilerExpression
en unestatic
méthode dans un assembly dynamique.Ainsi:
Ce n'est cependant pas idéal. Je ne suis pas tout à fait certain à quels types cela s'applique exactement, mais je pense que les types qui sont pris comme paramètres par le délégué ou renvoyés par le délégué doivent être
public
et non génériques. Il doit être non générique parce que les types génériques accèdent apparemmentSystem.__Canon
qui est un type interne utilisé par .NET sous le capot pour les types génériques et cela viole lapublic
règle "doit être une règle de type).Pour ces types, vous pouvez utiliser le plus lent
Compile
. Je les détecte de la manière suivante:Mais comme je l'ai dit, ce n'est pas l'idéal et j'aimerais quand même savoir pourquoi la compilation d'une méthode en un assemblage dynamique est parfois un ordre de grandeur plus rapide. Et je dis parfois parce que j'ai aussi vu des cas où un
Expression
compilé avecCompile
est tout aussi rapide qu'une méthode normale. Voir ma question pour cela.Ou si quelqu'un connaît un moyen de contourner la
public
contrainte «pas de non- types» avec l'assembly dynamique, c'est également le bienvenu.la source
Vos expressions ne sont pas équivalentes et vous obtenez ainsi des résultats biaisés. J'ai écrit un banc d'essai pour tester cela. Les tests incluent l'appel lambda régulier, l'expression compilée équivalente, une expression compilée équivalente faite à la main, ainsi que des versions composées. Ces chiffres devraient être plus précis. Fait intéressant, je ne vois pas beaucoup de variation entre les versions simples et composées. Et les expressions compilées sont naturellement plus lentes mais seulement de très peu. Vous avez besoin d'une entrée et d'un nombre d'itérations suffisamment importants pour obtenir de bons nombres. Cela fait une différence.
En ce qui concerne votre deuxième question, je ne sais pas comment vous pourriez en tirer plus de performances, donc je ne peux pas vous aider. Ça a l'air aussi beau que ça va l'être.
Vous trouverez ma réponse à votre troisième question dans la
HandMadeLambdaExpression()
méthode. Ce n'est pas l'expression la plus facile à construire en raison des méthodes d'extension, mais c'est faisable.Et les résultats sur ma machine:
la source
Les performances lambda compilées sur les délégués peuvent être plus lentes car le code compilé lors de l'exécution peut ne pas être optimisé, mais le code que vous avez écrit manuellement et celui compilé via le compilateur C # est optimisé.
Deuxièmement, plusieurs expressions lambda signifient plusieurs méthodes anonymes, et l'appel de chacune d'elles prend peu de temps supplémentaire par rapport à l'évaluation d'une méthode simple. Par exemple, appeler
et
sont différents, et avec une seconde, un peu plus de surcharge est nécessaire car du point de vue du compilateur, il s'agit en fait de deux appels différents. Appelez d'abord x lui-même, puis dans l'instruction d'appel de x.
Ainsi, votre Lambda combiné aura certainement peu de performances lentes par rapport à une seule expression lambda.
Et cela est indépendant de ce qui s'exécute à l'intérieur, car vous évaluez toujours la logique correcte, mais vous ajoutez des étapes supplémentaires à exécuter par le compilateur.
Même après la compilation de l'arbre d'expression, il n'aura pas d'optimisation, et il conservera toujours sa petite structure complexe, son évaluation et son appel peuvent avoir une validation supplémentaire, une vérification nulle, etc., ce qui pourrait ralentir les performances des expressions lambda compilées.
la source
UsingLambdaCombined
test combine plusieurs fonctions lambda et ses performances sont très proches deUsingLambda
. Concernant les optimisations, j'étais convaincu qu'elles étaient gérées par le moteur JIT, et donc le code généré à l'exécution (après compilation), serait également la cible de toute optimisation JIT.