CppDepend中的PRQL性能
CppDepend中的PRQL性能
PRQL 旨在针对大型真实代码库每秒运行数百个查询。这意味着大多数 PRQL 查询理论上应该在几毫秒内执行。在实践中,大多数查询确实如此,但如果您查看默认的 PRQL 查询和规则集,会发现其中少数在大型代码库上需要几十毫秒才能执行。
PRQL 查询执行时长的超时默认值为两秒,但可以在以下位置轻松更改此值 工具与选项与代码查询 面板。
在编写数十个默认 PRQL 规则和查询集时,我们调整了 PRQL 的设计,以确保即使是复杂的查询也能始终快速运行。
本文档分享了这项工作的成果。
性能是 PRQL 的重要主题,因为 CppDepend 工具的理念是尽快(几秒钟内)向用户提供有用的反馈。
始终追求线性时间复杂度
在编写需要某种嵌套处理的复杂查询时,最显而易见的方法通常是将一个查询嵌套在另一个查询中。下面的查询说明了这一点,我们希望匹配所有调用名为……的方法的方法 添加:
1from m in Methods23from users in Methods45where m.SimpleName == @"Add" && users.IsUsingMethod(m)67select users
这种方法的问题在于它会导致以缓慢的多项式时间复杂度执行的查询( O(#方法^2) 这里)。
在大多数情况下,可以将缓慢的多项式时间复杂度转换为线性时间复杂度。例如,我们的查询可以重写为:
1let addMethods =23 from m in Methods45 where m.SimpleName == @"Add"67 select m891011from m in addMethods1213from user in m.MethodsCallingMe1415select user
查询现在具有线性时间复杂度 O(#方法) 并且实际上它在几毫秒内执行完成,而不是几十秒!请注意,这里我们依赖于以下事实 PRQL允许查询以开始 让 子句.
尽可能使用序列使用操作
实际上,借助以下方法,上一节中获得的查询可以重写得更快、更简洁 UsingAny().
1Methods.UsingAny(Methods.WithSimpleName(@"Add")).Select(m => m)
让我们再举一个例子,匹配继承自命名空间中定义的任何接口的类型 MyNamespace。可以这样写:
1let types = Namespaces.WithName("MyNamespace").ChildTypes()23from t in Application.Types45from t2 in types67where t.DeriveFrom(t2)89select t
但通过使用扩展方法 ThatDeriveFromAny(),测试表明重写后的查询版本运行速度快了 10 倍。
1Types.ThatDeriveFromAny(23 Namespaces.WithName("MyNamespace").ChildTypes()45).Select(t => t)
这些扩展方法的内部优化基于这样一个事实:它们实际上替代了循环。因此,这种实现可以自由地依赖更智能的算法,比循环更快地过滤输入序列。
在主查询循环之前声明子集
如果您需要查询代码库的子集,请确保在主查询循环之前一次性定义该子集。
例如以下查询...
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
... 可以这样重写,速度提高 5 到 10 倍。
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
广泛依赖哈希集
该 System.Collections.Generic.HashSet
PRQL 提供了多个扩展方法,以便更有效地处理 HashSet
当查询依赖集合运算(并集、交集……)时,从性能角度考虑,通常最好将可枚举对象转换为哈希集。例如,通过移除对扩展方法的调用 ToHashSet(),下面的查询慢了 200 多倍!
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 }
避免在主查询循环中使用许多let子句
通过定义范围变量 让 子句 是 LINQ 提供的一种便捷语法。问题在于,这种语法便利可能会显著减慢查询执行速度,因为在底层,每个 让 子句会强制创建一个新对象并复制在其声明之前已获得的所有值。
所以我们这里有一个 权衡 这里涉及性能与语法优雅性之间的权衡。性能并不总是优先,例如我们决定将此默认规则的阈值保持为 3 让 子句...
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 }
……这比使用单个以下元素的不太优雅版本慢约两倍 让 子句:
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 }
大量字符串常量时的性能
可能会出现查询需要枚举代码元素名称列表以进行匹配的情况。例如:
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
在拥有 50,000 个类型的超大型代码库上,此查询最多需要 25 毫秒才能运行。可以进行一个小优化,以避免反复调用该属性 名称 在 t 通过使用 EqualsAny() 方法的重写:
1from t in Types where23t.Name.EqualsAny("int","Uint", "Int16","UInt16",45 "Int16","UInt16", "Byte","SByte",67 "Single","Double", "Decimal")89select t
现在,此版本的查询最多需要 20 毫秒才能运行。微小的性能提升被 9 个字符串参数被反复传递给方法这一事实所抵消 EqualsAny().
一个思路是使用的实例 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
不幸的是,这个版本要慢得多,最佳运行时间为 150 毫秒,因为在底层 让 子句会在每次循环时造成性能损失。如果我们需要与数十个字符串常量进行比较,这个使用以下内容的版本 HashSet 最终可能更快。
PRQL 提供 WithNameIn() 方法,可以这样使用:
1Types.WithNameIn("int","Uint", "Int16","UInt16",23 "Int16","UInt16", "Byte","SByte",45 "Single","Double", "Decimal").Select(t => t)
此版本现在快得多,最佳运行时间为 12 毫秒,因为它消除了对 LINQ 循环的需要,并在内部用基于……的更快循环替代了它 为了 语法,结合使用 HashSet
