PRQL-Leistung in CppDepend
PRQL-Leistung in CppDepend
Dieses Dokument setzt voraus, dass Sie mit LINQ-Syntax und das Dokument über gelesen haben PRQL-Syntax Sehen Sie sich auch die Wikipedia-Definition für Zeitkomplexität falls Sie nicht wissen, was dieses Konzept bedeutet.
PRQL ist darauf ausgelegt, Hunderte von Abfragen pro Sekunde auf einer großen realen Codebasis auszuführen. Das bedeutet, dass die meisten PRQL-Abfragen theoretisch in wenigen Millisekunden ausgeführt werden sollten. In der Praxis trifft dies auf die meisten Abfragen zu, aber wenn Sie sich den Satz der Standard-PRQL-Abfragen und -Regeln ansehen, werden Sie feststellen, dass einige davon auf großen Codebasen einige Dutzend Millisekunden benötigen.
Der Standardwert für das Timeout der PRQL-Abfrageausführungsdauer beträgt zwei Sekunden, kann aber leicht geändert werden in Tools & Optionen & Codeabfrage Panel.
Beim Schreiben der Dutzenden von Standard-PRQL-Regeln und -Abfragen haben wir das PRQL-Design angepasst, um sicherzustellen, dass selbst komplexe Abfragen immer schnell ausgeführt werden können.
Das Ergebnis dieser Arbeit wird im vorliegenden Dokument geteilt.
Performance ist ein wichtiges Thema für PRQL, da die Philosophie des CppDepend-Tools darin besteht, dem Benutzer so schnell wie möglich, in wenigen Sekunden, nützliches Feedback zu geben.
Streben Sie stets lineare Zeitkomplexität an
Beim Schreiben einer komplexen Abfrage, die eine Art verschachtelte Verarbeitung benötigt, ist der naheliegendste Ansatz oft, eine Abfrage in eine andere zu verschachteln. Dies wird durch die Abfrage unten veranschaulicht, bei der wir alle Methoden finden wollen, die eine beliebige Methode namens Hinzufügen:
1from m in Methods23from users in Methods45where m.SimpleName == @"Add" && users.IsUsingMethod(m)67select users
Das Problem bei diesem Ansatz ist, dass er zu Abfragen führt, die mit langsamer polynomialer Zeitkomplexität ausgeführt werden ( O(#Methode^2) hier ).
In den meisten Fällen ist es möglich, eine langsame polynomielle Zeitkomplexität in eine lineare Zeitkomplexität umzuwandeln. Unsere Abfrage kann beispielsweise umgeschrieben werden:
1let addMethods =23 from m in Methods45 where m.SimpleName == @"Add"67 select m891011from m in addMethods1213from user in m.MethodsCallingMe1415select user
Die Abfrage hat jetzt lineare Zeitkomplexität O(#Methoden) und konkret wird sie in wenigen Millisekunden ausgeführt statt in mehreren Dutzend Sekunden! Beachten Sie, dass wir uns hier darauf verlassen, dass PRQL erlaubt, dass eine Abfrage mit beginnt lassen Klausel.
Verwenden Sie nach Möglichkeit Sequenznutzungsoperationen
Tatsächlich kann die im obigen Abschnitt erhaltene Abfrage dank der Methode UsingAny().
1Methods.UsingAny(Methods.WithSimpleName(@"Add")).Select(m => m)
Nehmen wir ein weiteres Beispiel, um Typen zu matchen, die von einem beliebigen Interface im Namespace MyNamespace. Dies kann so geschrieben werden:
1let types = Namespaces.WithName("MyNamespace").ChildTypes()23from t in Application.Types45from t2 in types67where t.DeriveFrom(t2)89select t
Aber durch Verwendung der Erweiterungsmethode ThatDeriveFromAny() zeigen Tests, dass die umgeschriebene Abfrageversion 10-mal schneller läuft.
1Types.ThatDeriveFromAny(23 Namespaces.WithName("MyNamespace").ChildTypes()45).Select(t => t)
Die interne Optimierung dieser Erweiterungsmethoden basiert auf der Tatsache, dass sie tatsächlich eine Schleife ersetzen. Daher kann eine solche Implementierung auf einen intelligenteren Algorithmus zurückgreifen, um die Eingabesequenz schneller zu filtern als mit einer Schleife.
Deklarieren Sie Teilmengen vor der Hauptabfrageschleife
Wenn Sie über eine Teilmenge der Codebasis abfragen müssen, definieren Sie diese Teilmenge ein für alle Mal vor der Hauptabfrageschleife.
Zum Beispiel die folgende Abfrage...
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
... kann so umgeschrieben werden, dass es 5- bis 10-mal schneller ist.
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
Umfassend auf Hashsets setzen
Der System.Collections.Generic.HashSet
PRQL bietet mehrere Erweiterungsmethoden, um effektiver mit HashSet
Wenn eine Abfrage auf Mengenoperationen (Vereinigung, Schnittmenge...) beruht, ist es aus Performancegründen oft sinnvoll, Enumerables in Hashsets umzuwandeln. Zum Beispiel durch Entfernen des Aufrufs der Erweiterungsmethode ToHashSet() ist die folgende Abfrage mehr als 200-mal langsamer!
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 }
Vermeiden Sie viele let-Klauseln in der Hauptabfrageschleife
Definieren einer Bereichsvariablen über lassen Klausel ist eine praktische Syntaxmöglichkeit, die LINQ bietet. Das Problem ist, dass dieser Syntaxbonus die Abfrageausführung erheblich verlangsamen kann, da unter der Haube jede lassen Klausel erzwingt das Erstellen eines neuen Objekts und das Kopieren aller bereits vor seiner Deklaration erhaltenen Werte.
Wir haben hier also Kompromiss hier zwischen Performance und eleganter Syntax. Die Performance gewinnt nicht unbedingt; so haben wir uns beispielsweise entschieden, diese Standardregel mit 3 beizubehalten lassen Klauseln...
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 }
...das etwa zweimal langsamer ist als diese viel weniger elegante Version mit einem einzigen lassen Klausel:
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 }
Leistung bei vielen Zeichenfolgenkonstanten
Es kann vorkommen, dass eine Abfrage eine Liste von Code-Element-Namen aufzählen muss, um sie zu matchen. Zum Beispiel:
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
Auf einer sehr großen Codebasis mit 50.000 Typen benötigt diese Abfrage bestenfalls 25 ms zur Ausführung. Eine kleine Optimierung ist möglich, um das wiederholte Aufrufen der Eigenschaft zu vermeiden Name auf t durch Verwendung einer Überschreibung der Methode EqualsAny():
1from t in Types where23t.Name.EqualsAny("int","Uint", "Int16","UInt16",45 "Int16","UInt16", "Byte","SByte",67 "Single","Double", "Decimal")89select t
Nun benötigt diese Version der Abfrage bestenfalls 20 ms zur Ausführung. Der kleine Performancegewinn wird dadurch kompensiert, dass die 9 String-Parameter immer wieder an die Methode übergeben werden EqualsAny().
Eine Idee ist, eine Instanz von zu verwenden 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
Leider ist diese Version viel langsamer mit einer besten Laufzeit von 150 ms, denn unter der Haube lassen Klausel verursacht bei jeder Schleifeniteration einen Performance-Verlust. Stünden wir Dutzenden von String-Konstanten zum Vergleich gegenüber, wäre diese Version mit HashSet könnte am Ende schneller sein.
PRQL stellt die Methode WithNameIn() bereit, die so verwendet werden kann:
1Types.WithNameIn("int","Uint", "Int16","UInt16",23 "Int16","UInt16", "Byte","SByte",45 "Single","Double", "Decimal").Select(t => t)
Diese Version ist nun viel schneller mit einer besten Laufzeit von 12 ms, da sie die Notwendigkeit einer LINQ-Schleife beseitigt und diese intern durch eine schnellere Schleife ersetzt, die auf dem für Syntax, kombiniert mit der Verwendung von HashSet
Testen Sie CppDepend noch heute
Starten Sie Ihre 14-tägige kostenlose Testversion mit vollem Zugriff auf alle Dokumentationsfunktionen. Keine Kreditkarte erforderlich.
