PRQL Performance in CppDepend
PRQL Performance in CppDepend
This document assumes that you are familiar with the LINQ syntax and have read the document about the PRQL syntax. Also please have a look at the wikipedia definition for time complexity if you don't know what this notion means.
PRQL is designed to run hundreds of queries per seconds against a large real-world code base. This means that most PRQL queries should be executed in a few milliseconds in theory. In practices, this is true for most queries, but if you look at the set of default PRQL queries and rules, you'll see that a few of them are executed in a few dozens of milliseconds on large code bases.
The default value for the time-out for PRQL query execution duration is equals to two seconds, but this value is easily changeable in the Tools & Options & Code Query panel.
While writing the set of dozens of default PRQL rules and queries, we have adapted the PRQL design to make sure that it is always possible to run quickly even complex queries.
The result of this work is shared in the present document.
Performance is an important topic for PRQL, because the philosophy of the CppDepend tool is to provide useful feedbacks to the user as quickly as possible, in a few seconds.
Always strive for linear time complexity
When writing a complex query that needs some sort of nested processing, often the most obvious approach is to nest a query inside another one. This is illustrated by the query below, where we are interested to match all methods that calls any method named Add:
1from m in Methods23from users in Methods45where m.SimpleName == @"Add" && users.IsUsingMethod(m)67select users
The problem with this approach is that it leads to query that are executed in a slow polynomial time complexity ( O(#Method^2) here ).
In most cases it is possible to transform a slow polynomial time complexity, into a linear time complexity. For example our query can be rewritten:
1let addMethods =23 from m in Methods45 where m.SimpleName == @"Add"67 select m891011from m in addMethods1213from user in m.MethodsCallingMe1415select user
The query has now a linear time complexity O(#Methods) and concretely it gets executed in a few milliseconds, instead of several dozens of seconds! Notice that here we rely on the fact that PRQL allows a query to begin with a let clause.
Use sequence usage operations if possible
Actually, the query obtained in the section above can be rewritten to be even faster and more concise thanks to the method UsingAny().
1Methods.UsingAny(Methods.WithSimpleName(@"Add")).Select(m => m)
Let's take another example to match types that inherit from any interface defined in the namespace MyNamespace. This can be written this way:
1let types = Namespaces.WithName("MyNamespace").ChildTypes()23from t in Application.Types45from t2 in types67where t.DeriveFrom(t2)89select t
But by using the extension method ThatDeriveFromAny() tests shows that the rewritten version of query runs 10 times faster.
1Types.ThatDeriveFromAny(23 Namespaces.WithName("MyNamespace").ChildTypes()45).Select(t => t)
The internal optimization of these extension methods is based on the fact that they actually replace a loop. Hence such implementation is free to rely on a smarter algorithm to filter the input sequence faster than with a loop.
Declare sub-sets before the main query loop
If you need to query over a sub-set of the code base, make sure to define this sub-set once for all, before the main query loop.
For example the following query...
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
... can be rewritten this way, to be 5 to 10 times faster.
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
Rely extensively on hashset
The System.Collections.Generic.HashSet
PRQL offers several extension methods to work more effectively with the HashSet
When a query relies on set operations (union, intersection...) it is often performance wise to transform enumerables into hashsets. For example, by removing the call to the extension method ToHashSet(), the following queries is more than 200 times slower!
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 }
Avoid many let clauses in the main query loop
Defining a range variable through a let clause is a convenient syntax possibility offered by LINQ. The problem is that this syntax bonus can significantly slow down query execution because under the hood, each let clause forces to create a new object and copy all values already obtained before its declaration.
So we have here a trade-off here between performance and syntax elegance. The performance doesn't necessarily win, for example we decided to keep this default rule with 3 let 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 }
...that is around two times slower than this much less elegant version with a single let 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 with many strings constants
It might happen that a query needs to enumerate a list of code elements names to match them. For example:
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
On a very large code base with 50.000 types this query takes 25ms at best to run. A small optimization is possible to avoid calling again and again the property Name on t by using an override of the method EqualsAny():
1from t in Types where23t.Name.EqualsAny("int","Uint", "Int16","UInt16",45 "Int16","UInt16", "Byte","SByte",67 "Single","Double", "Decimal")89select t
Now, this version of the query takes at best 20ms to run. The small performance gain is compensated by the fact that the 9 string parameters are passed again and again to the method EqualsAny().
An idea is to use an instance of 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
Unfortunatly this version is much slower with a best run time equals to 150ms, because under the hood, the let clause provoques a performance hit for each loop. If we were facing dozens of string constants to compare with, this version with HashSet could end up being faster.
PRQL provides the method WithNameIn() that can be used this way:
1Types.WithNameIn("int","Uint", "Int16","UInt16",23 "Int16","UInt16", "Byte","SByte",45 "Single","Double", "Decimal").Select(t => t)
This version is now much faster with a best run time of 12ms because it removes the need for a LINQ loop, and internally replaces it with a faster loop based on the for syntax, coupled with the usage of a HashSet
Try CppDepend Today
Start your 14-day free trial with full access to all documentation features. No credit card required.
