この記事では、John Skeetが、最も単純な言語構成がプログラムの速度を低下させる方法と、それらを加速する方法について説明します。アプリケーションのパフォーマンスに関連する作業と同様に、結果は条件によって異なる場合があり(特に、たとえば64ビット
JIT
動作が少し異なる場合があります)、ほとんどの場合、これは心配する必要はありません。 それにもかかわらず、比較的少数の開発者が、多数のマイクロ最適化で構成されるプロダクションコードを記述しています。 したがって、この投稿を不合理な最適化のためにコードを複雑にする呼び出しとして受け取らないでください。これはおそらくあなたのプログラムを高速化します。 本当に必要な場合にのみ使用してください。
制限new()
たとえば、
SteppedPattern
型(著者はライブラリの例、
Noda Time
、約Transl。で最適化について説明します)があるとします。これはジェネリック型
TBucket
を持っています。 値を
TBucket
する
TBucket
に、
TBucket
クラスの新しいオブジェクトを作成することが重要であることに注意してください。 考えは、情報のビットが
Bucket
に積み重ねられて解析されるというものです。 そして、操作が完了すると、
ParseResult
して
ParseResult
ます。 したがって、すべての行解析操作には
TBucket
インスタンスが必要
TBucket
。 ジェネリック型の場合、どのように作成できますか?
これを行うには、パラメーターなしで型コンストラクターを呼び出します。 渡された型にそのようなコンストラクターがあるかどうかは考えたくないので、
new()
制約を追加して
new TBucket()
を呼び出します。
いいね! とても簡単です。 ただし、残念ながら、この1行のコードが行の解析にかかる時間の75%を占めるという事実を見失いました。 そして、これは空の
Bucket
作成にすぎません-最も単純な行を解析する最も単純なクラスです! これを理解したとき、私はショックを受けました。
プロバイダーの使用を修正します
修正は非常に簡単です。 オブジェクトをインスタンス化する方法を型に伝える必要があります。 デリゲートの助けを借りてこれを行います。
これで、
new StoppedPattern(() => new OffsetBucket())
またはそのようなものを呼び出すことができます。 また、コストラクタを内部として残し、二度と面倒を見ることができないことも意味します。 さらに、後続のコードの記述をさらに簡素化するために、古いバケットを使用して後続の行を解析することもできます。
タブレットが欲しい!
誰もが自分でテストを実行したいわけではありませんが、完成した結果を見たいと思っている人は多いようです。 したがって、ベンチマークの結果を提供することにしました。これは、ジェネリック型の作成時のみをチェックするために行いました。 これらの結果がどれほど重要でないかを示すために、表に記録された値がミリ秒単位で測定されることを示します。 この間に、1億回の操作が実行されました。これをテストします。 したがって、コードがジェネリック型を作成する操作を頻繁に呼び出すことに基づいていない限り、コードを書き換えることはありません。 ただし、将来のためにこれを覚えておいてください。
いずれにしても、私たちのコードは、2つのクラスと2つの構造の4つのタイプで動作するように設計されています。 そして、それらのそれぞれについて-CLR
CLR v2
、v4の32ビットおよび64ビットマシン上で、小規模および大規模バージョン(GAKの小規模および大規模バージョン、つまり85Kより小さいおよび大きいことを意味する) 私の64ビットマシン自体は高速なので、同じマシン内で結果を比較する必要があります。
CLR v4:32ビットの結果(1億回の反復あたりのミリ秒)CLR v4:64ビットの結果(1億回の反復あたりのミリ秒)CLR v2:32ビットの結果(1億回の反復あたりのミリ秒)CLR v2:64ビットの結果(1億回の反復あたりのミリ秒)クラスの結果を見てください。 これらは実際の結果です。
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
を渡します。 また、彼はいくつかのケースを含めました。
- ラムダ式を使用します:Action foo =()=>();
private static void Lambda<T>() { Action foo = () => {}; if (foo == null) { throw new Exception(); } }
- コンパイラに私にさせたいこと:クラスのインスタンスを作成するためのデリゲートを格納する個別のキャッシュ。
private static void FakeCachedLambda<T>() { if (FakeLambdaCache<T>.CachedAction == null) { FakeLambdaCache<T>.CachedAction = FakeLambdaCache<T>.NoOp; } Action foo = FakeLambdaCache<T>.CachedAction; if (foo == null) { throw new Exception(); } } private static class FakeLambdaCache<T> { internal static Action CachedAction; internal static void NoOp() {} }
- コンパイラが実際にラムダ式で行うこと:別のジェネリックメソッドを記述し、
method group conversion
を行います
private static void GenericMethodGroup<T>() { Action foo = NoOp<T>; if (foo == null) { throw new Exception(); } }
- コンパイラができること:個別の非
generic
メソッドを使用して、その後method group conversion
適用する
private static void NonGenericMethodGroup<T>() { Action foo = NoOp; if (foo == null) { throw new Exception(); } }
- 静的な非
generic
ジェネリック型メソッドでmethod group conversion
を使用します。
private static void StaticMethodOnGenericType<T>() { Action foo = SampleGenericClass<T>.NoOpStatic; if (foo == null) { throw new Exception(); } }
- 非静的非ジェネリックジェネリックメソッドで
method group conversion
を使用し、ジェネリッククラスのインスタンスを指す単一のフィールドを持つジェネリックキャッシュを使用します。
はい、後者は少し複雑に見えますが、はるかに簡単に見えます:
private static void InstanceMethodOnGenericType<T>() { Action foo = ClassHolder<T>.SampleInstance.NoOpInstance; if (foo == null) { throw new Exception(); } }
未解決の定義もすべて明らかにします。
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で実行されたことを明確にします
はい、ジェネリックパラメーターとして参照型を使用するジェネリックメソッドへのデリゲートの作成は、ジェネリックパラメーターとしての値型の場合よりも150倍遅くなります。 そして、私はそれについて最初に知っているようです。 もちろん、
CLR
チームのブログで答えを聞くのは非常に興味深いでしょう...
結論
テストがなかったら、この落とし穴を見つけることはできなかったでしょう。 この投稿から学べる教訓は、アプリケーションのパフォーマンスが目標であり、コードが多数の操作に依存してジェネリック型の新しいオブジェクトを選択する場合、
new()
制約を使用しないことです。
正確な答えを知ることが難しくなる最も難しい質問の1つは、コンパイラがラムダ式をどうするかということです。 私たちのバージョンでは、コンパイラーはパフォーマンスにあまり関心がなく、自分で処理する必要があります。
