CppDependのPRQLパフォーマンス
CppDependのPRQLパフォーマンス
このドキュメントは、読者が次に精通していることを前提としています LINQ構文 に関するドキュメントを読んだ PRQL構文 Wikipedia の次の定義も参照してください 時間計算量 この概念の意味を知らない場合。
PRQL は、大規模な実際のコードベースに対して 1 秒間に数百のクエリを実行するように設計されています。これは、理論上、ほとんどの PRQL クエリが数ミリ秒で実行されるべきであることを意味します。実際には、ほとんどのクエリでこれは当てはまりますが、既定の PRQL クエリとルールのセットを見ると、大規模なコードベースで数十ミリ秒かかるものもいくつかあることがわかります。
PRQL クエリ実行時間のタイムアウトのデフォルト値は 2 秒ですが、この値は次の場所で簡単に変更できます ツールとオプションとコードクエリ パネル。
数十の既定の 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 }
...これは、単一の次を使用するはるかに洗練度の低いバージョンより約 2 倍遅くなります させる 句:
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 ms です。内部的には させる 句はループごとにパフォーマンスの低下を引き起こします。比較対象の文字列定数が数十個ある場合、次を使用するこのバージョンは HashSet 結果的に速くなる可能性。
PRQL は次のように使用できる WithNameIn() メソッドを提供します:
1Types.WithNameIn("int","Uint", "Int16","UInt16",23 "Int16","UInt16", "Byte","SByte",45 "Single","Double", "Decimal").Select(t => t)
このバージョンは、LINQ ループの必要性をなくし、内部的にそれをより高速なループに置き換えたため、最良の実行時間 12 ミリ秒ではるかに高速になりました。 のために 構文との使用の組み合わせ HashSet
