CLRの最適化とジェネリック

この記事では、John Skeetが、最も単純な言語構成がプログラムの速度を低下させる方法と、それらを加速する方法について説明します。



アプリケーションのパフォーマンスに関連する作業と同様に、結果は条件によって異なる場合があり(特に、たとえば64ビットJIT



動作が少し異なる場合があります)、ほとんどの場合、これは心配する必要はありません。 それにもかかわらず、比較的少数の開発者が、多数のマイクロ最適化で構成されるプロダクションコードを記述しています。 したがって、この投稿を不合理な最適化のためにコードを複雑にする呼び出しとして受け取らないでください。これはおそらくあなたのプログラムを高速化します。 本当に必要な場合にのみ使用してください。





制限new()



たとえば、 SteppedPattern



型(著者はライブラリの例、 Noda Time



、約Transl。で最適化について説明します)があるとします。これはジェネリック型TBucket



を持っています。 値をTBucket



するTBucket



に、 TBucket



クラスの新しいオブジェクトを作成することが重要であることに注意してください。 考えは、情報のビットがBucket



に積み重ねられて解析されるというものです。 そして、操作が完了すると、 ParseResult



してParseResult



ます。 したがって、すべての行解析操作にはTBucket



インスタンスが必要TBucket



。 ジェネリック型の場合、どのように作成できますか?



これを行うには、パラメーターなしで型コンストラクターを呼び出します。 渡された型にそのようなコンストラクターがあるかどうかは考えたくないので、 new()



制約を追加してnew TBucket()



を呼び出します。



 // Somewhat simplified... internal sealed class SteppedPattern<TResult, TBucket> : IParsePattern<TResult>    where TBucket : new() {    public ParseResult<TResult> Parse(string value)   {       TBucket bucket = new TBucket();        // Rest of parsing goes here   } }
      
      







いいね! とても簡単です。 ただし、残念ながら、この1行のコードが行の解析にかかる時間の75%を占めるという事実を見失いました。 そして、これは空のBucket



作成にすぎません-最も単純な行を解析する最も単純なクラスです! これを理解したとき、私はショックを受けました。



プロバイダーの使用を修正します


修正は非常に簡単です。 オブジェクトをインスタンス化する方法を型に伝える必要があります。 デリゲートの助けを借りてこれを行います。

 // Somewhat simplified... internal sealed class SteppedPattern<TResult, TBucket> : IParsePattern<TResult> {    private readonly Func<TBucket> bucketProvider;    internal SteppedPattern(Func<TBucket> bucketProvider)   {        this.bucketProvider = bucketProvider;   }    public ParseResult<TResult> Parse(string value)   {       TBucket bucket = bucketProvider();        // Rest of parsing goes here   } }
      
      







これで、 new StoppedPattern(() => new OffsetBucket())



またはそのようなものを呼び出すことができます。 また、コストラクタを内部として残し、二度と面倒を見ることができないことも意味します。 さらに、後続のコードの記述をさらに簡素化するために、古いバケットを使用して後続の行を解析することもできます。



タブレットが欲しい!


誰もが自分でテストを実行したいわけではありませんが、完成した結果を見たいと思っている人は多いようです。 したがって、ベンチマークの結果を提供することにしました。これは、ジェネリック型の作成時のみをチェックするために行いました。 これらの結果がどれほど重要でないかを示すために、表に記録された値がミリ秒単位で測定されることを示します。 この間に、1億回の操作が実行されました。これをテストします。 したがって、コードがジェネリック型を作成する操作を頻繁に呼び出すことに基づいていない限り、コードを書き換えることはありません。 ただし、将来のためにこれを覚えておいてください。



いずれにしても、私たちのコードは、2つのクラスと2つの構造の4つのタイプで動作するように設計されています。 そして、それらのそれぞれについて-CLR CLR v2



、v4の32ビットおよび64ビットマシン上で、小規模および大規模バージョン(GAKの小規模および大規模バージョン、つまり85Kより小さいおよび大きいことを意味する) 私の64ビットマシン自体は高速なので、同じマシン内で結果を比較する必要があります。



CLR v4:32ビットの結果(1億回の反復あたりのミリ秒)

試験タイプ 新しい()制約 プロバイダーデリゲート
小さな構造 689 1225
大きな構造 11188 7273
少人数制 16307 1690
大人数 17471 3017




CLR v4:64ビットの結果(1億回の反復あたりのミリ秒)

試験タイプ 新しい()制約 プロバイダーデリゲート
小さな構造 473 868
大きな構造 2670 2396
少人数制 8366 1189
大人数 8805 1529




CLR v2:32ビットの結果(1億回の反復あたりのミリ秒)

試験タイプ 新しい()制約 プロバイダーデリゲート
小さな構造 703 1246
大きな構造 11411 7392
少人数制 143967 1791
大人数 143107 2581




CLR v2:64ビットの結果(1億回の反復あたりのミリ秒)

試験タイプ 新しい()制約 プロバイダーデリゲート
小さな構造 510 686
大きな構造 2334 1731
少人数制 81801 1539
大人数 83293 1896




クラスの結果を見てください。 これらは実際の結果です。 new()



制約を使用する場合はラップトップで約2分かかり、プロバイダーを使用する場合は数秒しかかかりません。 そして、これは非常に重要であり、これらの結果は.Net 2.0



関連してい.Net 2.0



(つまり、 CLR



意味し、バージョン2.0は.Net 2.0



までは.Net 3.5



まではすべてCLR v2



で動作するという事実に読者を驚かせるように書かれてい.Net 2.0



)。



そしてもちろん、 ベンチマークをダウンロードして、マシン上でどのように機能するかを確認できます。



「フードの下」で何が起きているのでしょうか?


私の知る限り、 new()



制約をサポートするIL



命令はありません。 代わりに、コンパイラーはActivator.CreateInstance [T]呼び出し命令を挿入します。 明らかに、これはデリゲートを呼び出すよりも遅くなります。 この場合、リフレクションを介して適切なコンストラクターを見つけて呼び出します。 最適化されていないことに本当に驚きました。 結局のところ、明らかな解決策はデリゲートを使用し、将来の使用のためにそれらをキャッシュすることです。 結局、彼らのソリューションはキャッシュが占有する追加のメモリを消費しないため、彼らが行った問題について議論することはしません。



もっとベンチマークが欲しい!!



(記事の第2部から取得)



ここでは、デリゲートを使用した作業のパフォーマンスを確認します。 また、それらをスピードアップしてみてください。

私のサイトからパフォーマンステスト用の完全なソースコードをダウンロードできます。 実際、ここでは、テストを書くたびに同じことをしています。 何もしないAction



デリゲートを作成し、それへのリンクが無効になっていないことを確認します。 これは、 JIT



最適化を回避するためだけに行います。 各テストは、1つの汎用パラメーターを受け取る汎用メソッドとして実行されます。 各メソッドを2回呼び出します。最初はInt32



を引数として渡し、2番目はString



を渡します。 また、彼はいくつかのケースを含めました。





未解決の定義もすべて明らかにします。

  private static void NoOp() {} private static void NoOp<T>() {} private class ClassHolder<T> { internal static SampleGenericClass<T> SampleInstance = new SampleGenericClass<T>(); } private class SampleGenericClass<T> { internal static void NoOpStatic() { } internal void NoOpInstance() { } }
      
      







これはすべてジェネリックメソッドで行い、 Int32



String



各タイプに対して呼び出すことに注意してください。 そして、重要なことは、変数をキャプチャしないことです。また、ジェネリックパラメーターはメソッド本体の実装のどの部分にも関与しません。



試験結果


繰り返しますが、結果はミリ秒単位で1000万回の操作で表示されます。 非常に遅いので、1億回の操作でそれらを起動したくありません。 また、テストがx64 JITで実行されたことを明確にします



テスト TestCase [int] テストケース[文字列]
ラムダ式 180 29684
汎用キャッシュクラス 90 288
ジェネリックメソッドグループの変換 184 30017
非ジェネリックメソッドグループの変換 178 189
ジェネリック型の静的メソッド 180 29276
ジェネリック型のインスタンスメソッド 202 299


はい、ジェネリックパラメーターとして参照型を使用するジェネリックメソッドへのデリゲートの作成は、ジェネリックパラメーターとしての値型の場合よりも150倍遅くなります。 そして、私はそれについて最初に知っているようです。 もちろん、 CLR



チームのブログで答えを聞くのは非常に興味深いでしょう...



結論



テストがなかったら、この落とし穴を見つけることはできなかったでしょう。 この投稿から学べる教訓は、アプリケーションのパフォーマンスが目標であり、コードが多数の操作に依存してジェネリック型の新しいオブジェクトを選択する場合、 new()



制約を使用しないことです。



正確な答えを知ることが難しくなる最も難しい質問の1つは、コンパイラがラムダ式をどうするかということです。 私たちのバージョンでは、コンパイラーはパフォーマンスにあまり関心がなく、自分で処理する必要があります。

画像







All Articles