Performance PRQL dans CppDepend
Performance PRQL dans CppDepend
Ce document suppose que vous êtes familier avec Syntaxe LINQ et avoir lu le document sur le Syntaxe PRQL Consultez aussi la définition Wikipedia de complexité temporelle si vous ne savez pas ce que cette notion signifie.
PRQL est conçu pour exécuter des centaines de requêtes par seconde sur une grande base de code réelle. Cela signifie que la plupart des requêtes PRQL devraient théoriquement s'exécuter en quelques millisecondes. En pratique, c'est vrai pour la plupart des requêtes, mais si vous regardez l'ensemble des requêtes et règles PRQL par défaut, vous verrez que quelques-unes s'exécutent en quelques dizaines de millisecondes sur de grandes bases de code.
La valeur par défaut du délai d'expiration pour la durée d'exécution des requêtes PRQL est de deux secondes, mais cette valeur est facilement modifiable dans Outils & options & requête de code le panneau.
Lors de l'écriture de l'ensemble des dizaines de règles et requêtes PRQL par défaut, nous avons adapté la conception de PRQL pour nous assurer qu'il est toujours possible d'exécuter rapidement même des requêtes complexes.
Le résultat de ce travail est partagé dans le présent document.
La performance est un sujet important pour PRQL, car la philosophie de l'outil CppDepend est de fournir des retours utiles à l'utilisateur le plus rapidement possible, en quelques secondes.
- Visez toujours une complexité temporelle linéaire
- Utilisez si possible des opérations d’utilisation de séquence
- Déclarez les sous-ensembles avant la boucle principale de la requête
- S’appuyer largement sur les hashsets
- Évitez de nombreuses clauses let dans la boucle principale
- Performance avec de nombreuses constantes chaînes
Visez toujours une complexité temporelle linéaire
Lors de l'écriture d'une requête complexe nécessitant une sorte de traitement imbriqué, l'approche la plus évidente est souvent d'imbriquer une requête dans une autre. Cela est illustré par la requête ci-dessous, où nous souhaitons trouver toutes les méthodes qui appellent une méthode nommée Ajouter:
1from m in Methods23from users in Methods45where m.SimpleName == @"Add" && users.IsUsingMethod(m)67select users
Le problème de cette approche est qu'elle conduit à des requêtes exécutées avec une lente complexité temporelle polynomiale ( O(#méthode^2) ici ).
Dans la plupart des cas, il est possible de transformer une complexité temporelle polynomiale lente en une complexité temporelle linéaire. Par exemple, notre requête peut être réécrite :
1let addMethods =23 from m in Methods45 where m.SimpleName == @"Add"67 select m891011from m in addMethods1213from user in m.MethodsCallingMe1415select user
La requête a désormais une complexité temporelle linéaire O(#méthodes) et concrètement, elle s'exécute en quelques millisecondes, au lieu de plusieurs dizaines de secondes ! Notez qu'ici nous nous appuyons sur le fait que PRQL permet à une requête de commencer par laisser clause.
Utilisez si possible des opérations d’utilisation de séquence
En fait, la requête obtenue dans la section ci-dessus peut être réécrite pour être encore plus rapide et concise grâce à la méthode UsingAny().
1Methods.UsingAny(Methods.WithSimpleName(@"Add")).Select(m => m)
Prenons un autre exemple pour faire correspondre les types qui héritent de n'importe quelle interface définie dans le namespace MyNamespace. Cela peut s’écrire ainsi :
1let types = Namespaces.WithName("MyNamespace").ChildTypes()23from t in Application.Types45from t2 in types67where t.DeriveFrom(t2)89select t
Mais en utilisant la méthode d'extension ThatDeriveFromAny(), les tests montrent que la version réécrite de la requête s'exécute 10 fois plus vite.
1Types.ThatDeriveFromAny(23 Namespaces.WithName("MyNamespace").ChildTypes()45).Select(t => t)
L'optimisation interne de ces méthodes d'extension repose sur le fait qu'elles remplacent en réalité une boucle. Une telle implémentation est donc libre de s'appuyer sur un algorithme plus intelligent pour filtrer la séquence d'entrée plus rapidement qu'avec une boucle.
Déclarez les sous-ensembles avant la boucle principale de la requête
Si vous devez interroger un sous-ensemble de la base de code, assurez-vous de définir ce sous-ensemble une fois pour toutes, avant la boucle principale de la requête.
Par exemple la requête suivante...
1from m in Application.Methods where23 m.IsUsing("MyClass.MyMethod()".AllowNoMatch()) ||45 m.IsUsing("MyClass.MyMethod(int)".AllowNoMatch()) ||67 m.IsUsing("MyClass.MyMethod(int,int)".AllowNoMatch())89select m
... peut être réécrite de cette façon pour être 5 à 10 fois plus rapide.
1let gcCollectMethods = ThirdParty.Methods.WithFullNameIn(23 "MyClass.MyMethod()",45 "MyClass.MyMethod(int)",67 "MyClass.MyMethod(int,int)")89from m in Application.Methods.UsingAny(gcCollectMethods)1011select m
S’appuyer largement sur les hashsets
Le System.Collections.Generic.HashSet
PRQL offre plusieurs méthodes d'extension pour travailler plus efficacement avec HashSet
Lorsqu'une requête repose sur des opérations ensemblistes (union, intersection...), il est souvent judicieux, en termes de performance, de transformer les énumérables en hashsets. Par exemple, en supprimant l'appel à la méthode d'extension ToHashSet() la requête suivante est plus de 200 fois plus lente !
1// <Name>Callers of refactored methods</Name>23let refactoredMethods = Application.Methods.Where(m => m.CodeWasChanged()).ToHashSet()45from caller in Application.Methods.UsingAny(refactoredMethods)67let refactoredMethodsCalled = caller.MethodsCalled.Intersect(refactoredMethods)89where refactoredMethodsCalled.Count() > 01011select new { caller, refactoredMethodsCalled }
Évitez de nombreuses clauses let dans la boucle principale
Définir une variable de portée via un laisser clause est une possibilité de syntaxe pratique offerte par LINQ. Le problème est que ce bonus de syntaxe peut ralentir considérablement l'exécution de la requête car sous le capot, chaque laisser clause force la création d'un nouvel objet et la copie de toutes les valeurs déjà obtenues avant sa déclaration.
Nous avons donc ici un compromis ici entre performance et élégance de la syntaxe. La performance ne l'emporte pas nécessairement ; par exemple, nous avons décidé de conserver cette règle par défaut avec 3 laisser clauses...
1// <Name>CRAP methods</Name>23// Source: http://www.artima.com/weblogs/viewpost.jsp?thread=21589945from method in Application.Methods67where method.CyclomaticComplexity != null && method.PercentageCoverage != null89let CC = method.CyclomaticComplexity1011let uncov = (100 - method.PercentageCoverage) / 100f1213let CRAP = (CC * CC * uncov * uncov * uncov) + CC1415where CRAP > 301617orderby CRAP descending, method.NbLinesOfCode descending1819select new { method, CRAP, CC, uncov, method.PercentageCoverage, method.NbLinesOfCode }
...qui est environ deux fois plus lente que cette version bien moins élégante avec un seul laisser clause :
1// <Name>CRAP methods</Name>23// Source: http://www.artima.com/weblogs/viewpost.jsp?thread=21589945from method in Application.Methods67where method.CyclomaticComplexity != null && method.PercentageCoverage != null89let CRAP = (method.CyclomaticComplexity * method.CyclomaticComplexity *1011 ((100 - method.PercentageCoverage) / 100f)*1213 ((100 - method.PercentageCoverage) / 100f)*1415 ((100 - method.PercentageCoverage) / 100f)) + method.CyclomaticComplexity1617where CRAP > 301819orderby CRAP descending, method.NbLinesOfCode descending2021select new { method,2223 CRAP,2425 CC = method.CyclomaticComplexity ,2627 uncov = ((100 - method.PercentageCoverage) / 100f),2829 method.PercentageCoverage,3031 method.NbLinesOfCode }
Performance avec de nombreuses constantes chaînes
Il peut arriver qu'une requête ait besoin d'énumérer une liste de noms d'éléments de code pour les faire correspondre. Par exemple :
1from t in Types where23t.Name == "int" || t.Name == "Uint" || t.Name == "Int16" || t.Name == "UInt16" ||45t.Name == "Int64" || t.Name == "UInt64" || t.Name == "Byte" || t.Name == "SByte" ||67t.Name == "Single" || t.Name == "Double" || t.Name == "Decimal"89select t
Sur une très grande base de code avec 50 000 types, cette requête prend au mieux 25 ms à s'exécuter. Une petite optimisation est possible pour éviter d'appeler encore et encore la propriété Nom sur t en utilisant une surcharge de la méthode EqualsAny() :
1from t in Types where23t.Name.EqualsAny("int","Uint", "Int16","UInt16",45 "Int16","UInt16", "Byte","SByte",67 "Single","Double", "Decimal")89select t
Maintenant, cette version de la requête prend au mieux 20 ms à s'exécuter. Le petit gain de performance est compensé par le fait que les 9 paramètres string sont passés encore et encore à la méthode EqualsAny().
Une idée est d’utiliser une instance de HashSet
1let hashset = new [] { "int","Uint", "Int16","UInt16",23 "Int16","UInt16", "Byte","SByte",45 "Single","Double", "Decimal" }.ToHashSet()67from t in Types where89hashset.Contains(t.Name)1011select t
Malheureusement, cette version est bien plus lente avec un meilleur temps d'exécution de 150 ms, car sous le capot, laisser clause provoque une perte de performance à chaque boucle. Si nous devions comparer avec des dizaines de constantes chaînes, cette version avec HashSet pourrait finir par être plus rapide.
PRQL fournit la méthode WithNameIn() qui peut s'utiliser ainsi :
1Types.WithNameIn("int","Uint", "Int16","UInt16",23 "Int16","UInt16", "Byte","SByte",45 "Single","Double", "Decimal").Select(t => t)
Cette version est maintenant beaucoup plus rapide avec un meilleur temps d'exécution de 12 ms, car elle supprime le besoin d'une boucle LINQ et la remplace en interne par une boucle plus rapide basée sur le pour syntaxe, combinée à l’utilisation d’un HashSet
Essayez CppDepend aujourd'hui
Commencez votre essai gratuit de 14 jours avec accès complet à toutes les fonctionnalités de documentation. Sans carte bancaire.
