1つのIoCコンテナーの最適化の歴史

この記事では、小さいながらも非常に有用なプロジェクトについて情報を共有したいと思います。StefánJökullSigurðarsonは、.NET Coreに移行したことがわかっているすべてのIoCコンテナーを追加し、 BenchmarkDotNetを使用したインスタンス測定解決を使用しますパフォーマンス。 私はこのコンペティションに参加する機会を逃しませんでした 。私は自分の小さなプロジェクトFsContainerに参加しました


画像


1.2.0


プロジェクトが.NET Coreに移行された後(私はそれが絶対に難しくないことが判明したことに注意したい)、私は失望しなかったと言って、それは何も言わないことを意味し、これは3つの測定値の1つが私のコンテナーを通過しなかったという事実によるものでした。 この言葉の直接的な意味では、測定は単純に20分以上続き、終了しませんでした。


その理由は次のコードセクションにあります。


public object Resolve(Type type) { var instance = _bindingResolver.Resolve(this, GetBindings(), type); if (!_disposeManager.Contains(instance)) { _disposeManager.Add(instance); } return instance; } 

考えてみると、ベンチマーク操作の主な原則は、単位時間(オプションのメモリ消費量)ごとに実行される操作の数を測定することです。つまり、 Resolveメソッドはできるだけ多く実行されます。 container.Dispose()場合、解決後、結果のインスタンスが_disposeManagerに追加されてさらに破棄されることに気付くかもしれません。 なぜなら 実装の内部にはList<object> 、そのインスタンスはContainsチェックすることで追加さContains 、すぐに2つの副作用があると推測できContains


  1. Containsチェックを使用して新しく作成された各インスタンスは、 GetHashCodeを計算し、以前に追加されたインスタンスの重複を探します。
  2. なぜなら 新しく作成された各インスタンスは常に一意になり(解決はTransientLifetimeManagerでテストされました)、 List<object>サイズは、メモリの新しい2倍の部分を割り当て、 以前に追加された要素をコピーして (メモリ割り当て操作の100万インスタンスを追加するため)常に増加しますコピーは少なくとも20回呼び出されます);

率直に言って、この場合、どのソリューションが最も正しいかわかりません。実際には、1つのコンテナが以前に作成されたインスタンスへの数百万のリンクを保持することを想像するのは難しいため、(かなり論理的な)制限を追加することで問題の半分しか解決しませんでしたIDisposableを実装するオブジェクトのみを_disposeManager追加し_disposeManager


 if (instance is IDisposable && !_disposeManager.Contains(instance)) { _disposeManager.Add(instance); } 

その結果、測定はかなり許容できる時間で完了し、次の結果が得られました。


方法平均エラーStddevスケーリング済みScaledSDGen 0Gen 1割り当て済み
直接13.77 ns0.3559 ns0.3655 ns1.000.000.0178-56 B
ライトインジェクト36.95 ns0.1081 ns0.0902 ns2.690.070.0178-56 B
シンプルインジェクター46.17 ns0.2746 ns0.2434 ns3.350.090.0178-56 B
アスプネットコア71.09 ns0.4592 ns0.4296 ns5.170.140.0178-56 B
Autofac1,600.67 ns14.4742 ns12.8310 ns116.323.100.5741-1803 B
構造図1,815.87 ns18.2271 ns16.1578 ns131.953.550.6294-1978 B
Fscontainer2,819.01 ns6.0161 ns5.3331 ns204.855.240.4845-1524 B
Ninject12,812.70 ns255.5191 ns447.5211 ns931.0639.951.78530.44255767 B

もちろん、私はそれらに満足しておらず、さらなる最適化方法を探し始めました。


1.2.1


コンテナの現在のバージョンでは、必要なコンストラクターとそれに必要な引数の定義は変更されていないため、この情報をキャッシュでき、今後CPU時間を浪費することはありません。 この最適化の結果、 ConcurrentDictionaryが追加され、そのキーは要求されたタイプ( Resolve<T> )であり、値は直接インスタンス化するために使用されるコンストラクターと引数です。


 private readonly IDictionary<Type, Tuple<ConstructorInfo, ParameterInfo[]>> _ctorCache = new ConcurrentDictionary<Type, Tuple<ConstructorInfo, ParameterInfo[]>>(); 

行われた測定から判断すると、このような単純な操作により、生産性が30%以上向上しました。


方法平均エラーStddevスケーリング済みScaledSDGen 0Gen 1Gen 2割り当て済み
直接13.50 ns0.2240 ns0.1986 ns1.000.000.0178--56 B
ライトインジェクト36.94 ns0.0999 ns0.0886 ns2.740.040.0178--56 B
シンプルインジェクター46.40 ns0.3409 ns0.3189 ns3.440.050.0178--56 B
アスプネットコア70.26 ns0.4897 ns0.4581 ns5.210.080.0178--56 B
Autofac1,634.89 ns15.3160 ns14.3266 ns121.142.010.5741--1803 B
Fscontainer1,779.12 ns18.9507 ns17.7265 ns131.832.270.2441--774 B
構造図1,830.01 ns5.4174 ns4.8024 ns135.601.970.6294--1978 B
Ninject12,558.59 ns268.1920 ns490.4042 ns930.5838.291.78580.44230.00055662 B

1.2.2


測定を行うとき、BenchmarkDotNetは、アセンブリが最適化されていない可能性があることをユーザーに通知します(デバッグ構成でアセンブル)。 長い間、このメッセージがnugetパッケージを使用してコンテナに接続されているプロジェクトでこのメッセージが表示された理由と、nugetパックのパラメーターの可能なリストを見たときの驚きは理解できませんでした:


 nuget pack MyProject.csproj -properties Configuration=Release 

デバッグ構成でパッケージを収集していた間、更新された測定結果から判断すると、パフォーマンスが最大で25%低下したことがわかりました。


方法平均エラーStddevスケーリング済みScaledSDGen 0Gen 1Gen 2割り当て済み
直接13.38 ns0.2216 ns0.2073 ns1.000.000.0178--56 B
ライトインジェクト36.85 ns0.0577 ns0.0511 ns2.750.040.0178--56 B
シンプルインジェクター46.56 ns0.5329 ns0.4724 ns3.480.060.0178--56 B
アスプネットコア70.17 ns0.1403 ns0.1312 ns5.250.080.0178--56 B
Fscontainer1,271.81 ns4.0828 ns3.8190 ns09.091.440.2460--774 B
Autofac1,648.52 ns2.3197 ns2.0563 ns123.261.840.5741--1803 B
構造マップ1,829.05 ns17.8238 ns16.6724 ns136.752.370.6294--1978 B
Ninject12,520.08 ns248.2530 ns534.3907 ns936.1041.981.78600.44230.00085662 B

1.2.3


もう1つの最適化は、アクティベーター関数のキャッシュで、Expressionを使用してコンパイルされます。


 private readonly IDictionary<Type, Func<object[], object>> _activatorCache = new ConcurrentDictionary<Type, Func<object[], object>>(); 

汎用関数は、引数ConstructorInfoと引数ParameterInfo[]配列を受け取り、結果として厳密に型指定されたラムダを返します。


 private Func<object[], object> GetActivator(ConstructorInfo ctor, ParameterInfo[] parameters) { var p = Expression.Parameter(typeof(object[]), "args"); var args = new Expression[parameters.Length]; for (var i = 0; i < parameters.Length; i++) { var a = Expression.ArrayAccess(p, Expression.Constant(i)); args[i] = Expression.Convert(a, parameters[i].ParameterType); } var b = Expression.New(ctor, args); var l = Expression.Lambda<Func<object[], object>>(b, p); return l.Compile(); } 

この解決策の論理的な継続は、ActivatorだけでなくResolve関数全体をコンパイルすることであることに同意します


方法平均エラーStddevスケーリング済みScaledSDGen 0Gen 1Gen 2割り当て済み
直接13.24 ns0.0836 ns0.0698 ns1.000.000.0178--56 B
ライトインジェクト37.39 ns0.0570 ns0.0533 ns2.820.010.0178--56 B
シンプルインジェクター46.22 ns0.2327 ns0.2063 ns3.490.020.0178--56 B
アスプネットコア70.53 ns0.2885 ns0.2698 ns5.330.030.0178--56 B
Fscontainer1,038.13 ns17.1037 ns15.9988 ns78.411.230.2327--734 B
Autofac1,551.33 ns3.6293 ns3.2173 ns117.170.640.5741--1803 B
構造マップ1,944.35 ns1.8665 ns1.7459 ns146.850.760.6294--1978 B
Ninject13,139.70 ns260.8754 ns508.8174 ns992.4338.351.78570.44250.00045682 B

1.2.4


記事の公開後、 @ turbanoffは、 ConcurrentDictionaryの場合、 GetOrAddメソッドのパフォーマンスがContainsKey / Add ConcurrentDictionaryパフォーマンスよりも高いことにGetOrAddました。 測定結果を以下に示します。


宛先:


 if (!_activatorCache.ContainsKey(concrete)) { _activatorCache[concrete] = GetActivator(ctor, parameters); } 

方法平均エラーStddev中央値Gen 0割り当て済み
シングルトンを解決する299.0 ns7.239 ns19.45 ns295.7 ns0.1268199 B
ResolveTransient686.3 ns32.333 ns86.30 ns668.7 ns0.2079327 B
ResolveCombined1,487.4 ns101.057 ns273.21 ns1,388.7 ns0.4673734 B

後:


 var activator = _activatorCache.GetOrAdd(concrete, x => GetActivator(ctor, parameters)); 

方法平均エラーStddevGen 0割り当て済み
シングルトンを解決する266.6 ns4.955 ns4.393 ns0.1268199 B
ResolveTransient512.0 ns16.974 ns16.671 ns0.3252511 B
ResolveCombined1,119.2 ns18.218 ns15.213 ns0.69431101 B

PS


実験として、さまざまなデザインを使用してオブジェクトの作成時間を測定することにしました。 プロジェクト自体はGithubで入手でき、以下の結果を見ることができます。 完全を期すために、欠落しているのは、Directメソッドに可能な限り近いIL命令を生成するアクティベーションメソッドです。これは、トップ4のコンテナーで使用されるメソッドであり、このような印象的な結果を達成できます


方法平均エラーStddevGen 0割り当て済み
直接4.031 ns0.1588 ns0.1890 ns0.007624 B
Compiledinvoke85.541 ns0.5319 ns0.4715 ns0.017856 B
ConstructorInfoInvoke316.088 ns1.8337 ns1.6256 ns0.027788 B
ActivatorCreateInstance727.547 ns2.9228 ns2.5910 ns0.1316416 B
Dynamicinvoke974.699 ns5.5867 ns5.2258 ns0.0515168 B


Source: https://habr.com/ru/post/J331584/


All Articles